diff --git a/.gitignore b/.gitignore index 91efc16b7..9bcb005c7 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ proguard/ *.iml MPChartLib + +fw.dirs diff --git a/.travis.yml b/.travis.yml index 22d9469af..d58ee5f86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,12 @@ language: android + +jdk: + - oraclejdk8 + - oraclejdk7 + +env: + - GRADLE_OPTS="-XX:MaxPermSize=256m" + android: components: # Uncomment the lines below if you want to @@ -7,7 +15,7 @@ android: - tools # The BuildTools version used by your project - - build-tools-23.0.2 + - build-tools-23.0.3 # The SDK version used to compile your project - android-23 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7213e752a..bf801f266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,92 @@ ###Changelog +####Version 0.9.7 +* Pebble: hopefully fix some reconnect issues +* Mi Band: fix live activity monitoring running forever if back button pressed +* Mi Band: allow low latency firmware updates, fixes update with some phones +* Mi Band: inital experimental and probably broken support for Amazfit +* Show aliases for BT Devices if they had been renamed in BT Settings +* Do not show a hint about App Manager when a Mi Band is connected + +####Version 0.9.6 +* Again some UI/theme improvements +* New preference to reconnect after connection loss (defaults to true) +* Fix crash when dealing with certain old preference values +* Mi Band: automatically reconnect when back in range after connection loss +* Mi Band 1S: display heart rate value again when invoked via the Debug view + +####Version 0.9.5 +* Several UI Improvements +* Easier First-time setup by using a FAB +* Optional Dark Theme +* Notification App Blacklist is now sorted +* Gadgetbridge Icon in the notification bar displays connection state +* Logging is now configurable without restart +* Mi Band 1S: Initial live heartrate tracking +* Fix certain crash in charts activity on slower devices (#277) + +####Version 0.9.4 +* Pebble: support pebble health datalog messages of firmware 3.11 (this adds support for deep sleep!) +* Pebble: try to reconnect on new notifications and phone calls when connection was lost unexpectedly +* Pebble: delay between reconnection attempts (from 1 up to 64 seconds) +* Fix crash in charts activities when changing the date, quickly (#277) +* Mi Band: preference to enable heart rate measurement during sleep (#232, thanks computerlyrik!) +* Mi Band: display measured heart rate in charts (#232) +* Mi Band 1S: full support for firmware upgrade/downgrade (both for Mi Band and heart rate sensor) (#234) +* Mi Band 1S: fix device detection for certain versions + +####Version 0.9.3 +* Pebble: Fix Pebble Health activation (was not available in the App Manager) +* Simplify connection state display (only connecting->connected) +* Small improvements to the pairing activity +* Mi Band 1S: Fix for mi band firmware update + +####Version 0.9.2 +* Mi Band: Fix update of second (HR) firmware on Mi1S (#234) +* Fix ordering issue of device infos being displayed + +####Version 0.9.1 +* Mi Band: fix sporadic connection problems (stuck on "Initializing" #249) +* Mi Band: enable low latency connection (faster) during initialization and activity sync +* Mi Band: better feedback for firmware update +* Device Item is now clickable also when the information entries are visible +* Fix enabling log file writing #261 + +####Version 0.9.0 +* Pebble: Support for configuring watchfaces/apps locally (clay) or though webbrowser (some do not work) +* Pebble: hide the alarm management activity as it's unsupported +* Mi Band: Improve firmware detection and updates, including 1S support +* Mi Band: Display HR FW for 1S +* FW and HW versions are only displayed after tapping on the "info" button in Control Center +* Do not display activity samples when navigating too far in the past +* Fix auto connect which was broken under some circumstances + +####Version 0.8.2 +* Fix database creation and updates (thanks @feclare) +* Add experimental widget to set the alarm time to a configurable number of hours in the future (thanks @0nse) +* Use ckChangeLog to display the Changelog within Gadgetbridge +* Workaround to fix logfile rotation (bug in logback-android) + +####Version 0.8.1 +* Pebble: install (and start) freshly-installed apps on the watch instead of showing a Toast that tells the user to do so. (only applies to firmware 3.x) +* Pebble: fix crash while receiving Health data +* Mi Band 1S: support for synchronizing activity data (#205) +* Mi Band 1S: support for reading the heart rate via the "Debug Screen" #178 + +####Version 0.8.0 +* Pebble: Support Pebble Health: steps/activity data are stored correctly. Sleep time is considered as light sleep. Deep sleep is discarded. The pebble will send data where it seems appropriate, there is no action to perform on the watch for this to happen. +* Pebble: Fix support for newer version of morpheuz (>=3.3?) +* Pebble: Allow to select the preferred activity tracker via settings activity (Health, Misfit, Morpheuz) +* Pebble: Fix wrong(previous) contact being displayed on the pebble +* Mi Band: improvements to pairing and connecting +* Fix a problem related to shared preferences storage of activity settings +* Very basic support Android 6 runtime permission +* Fix layout of the alarms activity + +####Version 0.7.4 +* Refactored the settings activity: User details are now generic instead of miband specific. Old settings are preserved. +* Pebble: Fix regression with broken active reconnect since 0.7.0 +* Pebble: Support activation and deactivation of Pebble Health. Activation uses the User details as seen above. Insigths are NOT activated. + Please be aware that deactivation does NOT delete the data stored on the watch (but it seems to stop the tracking), and we do not know how to switch to metric length units. ####Version 0.7.3 * Pebble: Report connection state to PebbleKit companion apps via content provider. NOTE: Makes Gadgetbridge mutual exclusive with the original Pebble app. diff --git a/README.md b/README.md index 187372dd0..9367cd440 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ need to create an account and transmit any of your data to the vendor's servers. [List of changes](CHANGELOG.md) +## Supported Devices +* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round +* Mi Band, Mi Band 1A, Mi Band 1S (experimental) + ## Features (Pebble) * Incoming calls notification and display @@ -31,20 +35,22 @@ need to create an account and transmit any of your data to the vendor's servers. * Install firwmare files (.pbz) [READ THE WIKI](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble-Firmware-updates) * Install language files (.pbl) * Take and share screenshots from the Pebble's screen -* PebbleKit support for 3rd Party Android Apps support (experimental) -* Morpheuz sleep data syncronization (experimental) -* Misfit steps data synchronization (experimental) +* PebbleKit support for 3rd Party Android Apps (experimental) +* Fetch activity data from Pebble Health, Misfit and Morpheuz (experimental) +* Configure watchfaces / apps (limited compatibility, experimental) ## Notes about Firmware 3.x (Pebble Time, updated OG) * Listing installed watchfaces will simply display previously installed watchapps, no matter if they are still installed or not. -## How to use (Pebble) +## Getting Started (Pebble) -1. Pair your Pebble through Gadgetbridge's Discovery Activity or the Android Bluetooth Settings +1. Pair your Pebble through the Android's Bluetooth Settings 2. Start Gadgetbridge, tap on the device you want to connect to 3. To test, choose "Debug" from the menu and play around +For more information read [this wiki article](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Getting-Started-(Pebble)) + ## Features (Mi Band) * Mi Band notifications (LEDs + vibration) for @@ -55,6 +61,8 @@ need to create an account and transmit any of your data to the vendor's servers. * Generic Android notifications * Synchronize the time to the Mi Band * Display firmware version and battery state +* Firmware Update +* Heartrate Measurement (alpha) * Synchronize activity data * Display sleep data (alpha) * Display sports data (step count) (alpha) @@ -95,9 +103,12 @@ Contributions are welcome, be it feedback, bugreports, documentation, translatio on any of the open [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues?q=is%3Aopen+is%3Aissue); just leave a comment that you're working on one to avoid duplicated work. +Please do not use the issue tracker as a forum, do not ask for ETAs and read the issue conversation before posting. + Translations can be contributed via https://www.transifex.com/projects/p/gadgetbridge/resource/strings/ or manually. + ## Having problems? 1. Open Gadgetbridge's settings and check the option to write log files diff --git a/app/build.gradle b/app/build.gradle index ea5621ac1..fffe7f3cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,9 +4,11 @@ apply plugin: 'pmd' def ABORT_ON_CHECK_FAILURE=false +tasks.withType(Test) { systemProperty 'MiFirmwareDir', System.getProperty('MiFirmwareDir', null) } + android { compileSdkVersion 23 - buildToolsVersion "23.0.2" + buildToolsVersion "23.0.3" defaultConfig { applicationId "nodomain.freeyourgadget.gadgetbridge" @@ -14,8 +16,8 @@ android { targetSdkVersion 23 // note: always bump BOTH versionCode and versionName! - versionName "0.7.3" - versionCode 39 + versionName "0.9.7" + versionCode 51 } buildTypes { release { @@ -44,12 +46,14 @@ dependencies { testCompile "org.mockito:mockito-core:1.9.5" compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:23.1.1' - compile 'com.android.support:support-v4:23.1.1' + compile 'com.android.support:appcompat-v7:23.3.0' + compile 'com.android.support:support-v4:23.3.0' + compile 'com.android.support:design:23.3.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:v2.1.6' + compile 'com.github.PhilJay:MPAndroidChart:v2.2.4' compile 'com.github.pfichtner:durationformatter:0.1.1' + compile 'de.cketti.library.changelog:ckchangelog:1.2.2' } check.dependsOn 'findbugs', 'pmd', 'lint' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8d4aacd1f..436e04230 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,7 +24,6 @@ - @@ -48,25 +47,27 @@ + android:parentActivityName=".activities.ControlCenter" /> + android:parentActivityName=".activities.SettingsActivity" /> + android:parentActivityName=".activities.ControlCenter" /> + android:parentActivityName=".activities.SettingsActivity" /> + @@ -87,7 +88,6 @@ - @@ -98,7 +98,6 @@ - @@ -109,7 +108,6 @@ - @@ -123,6 +121,7 @@ + @@ -142,7 +141,6 @@ - @@ -153,7 +151,6 @@ - @@ -164,7 +161,6 @@ - @@ -180,10 +176,11 @@ + + - + android:windowSoftInputMode="stateHidden" /> + android:parentActivityName=".activities.ControlCenter" /> @@ -251,16 +248,47 @@ + android:parentActivityName=".activities.ControlCenter" /> + android:parentActivityName=".activities.SettingsActivity" /> - + android:parentActivityName=".activities.ConfigureAlarms" /> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/app_config/configure.html b/app/src/main/assets/app_config/configure.html new file mode 100644 index 000000000..14e4d7fd0 --- /dev/null +++ b/app/src/main/assets/app_config/configure.html @@ -0,0 +1,56 @@ + + + + + + + + + + +
+

Url of the configuration:

+
+ + +
+
+

Incoming configuration data:

+
+ +
+ diff --git a/app/src/main/assets/app_config/js/Uri.js b/app/src/main/assets/app_config/js/Uri.js new file mode 100644 index 000000000..a3fe49982 --- /dev/null +++ b/app/src/main/assets/app_config/js/Uri.js @@ -0,0 +1,458 @@ +/*! + * jsUri + * https://github.com/derek-watson/jsUri + * + * Copyright 2013, Derek Watson + * Released under the MIT license. + * + * Includes parseUri regular expressions + * http://blog.stevenlevithan.com/archives/parseuri + * Copyright 2007, Steven Levithan + * Released under the MIT license. + */ + + /*globals define, module */ + +(function(global) { + + var re = { + starts_with_slashes: /^\/+/, + ends_with_slashes: /\/+$/, + pluses: /\+/g, + query_separator: /[&;]/, + uri_parser: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*)(?::([^:@\/]*))?)?@)?(\[[0-9a-fA-F:.]+\]|[^:\/?#]*)(?::(\d+|(?=:)))?(:)?)((((?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ + }; + + /** + * Define forEach for older js environments + * @see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach#Compatibility + */ + if (!Array.prototype.forEach) { + Array.prototype.forEach = function(callback, thisArg) { + var T, k; + + if (this == null) { + throw new TypeError(' this is null or not defined'); + } + + var O = Object(this); + var len = O.length >>> 0; + + if (typeof callback !== "function") { + throw new TypeError(callback + ' is not a function'); + } + + if (arguments.length > 1) { + T = thisArg; + } + + k = 0; + + while (k < len) { + var kValue; + if (k in O) { + kValue = O[k]; + callback.call(T, kValue, k, O); + } + k++; + } + }; + } + + /** + * unescape a query param value + * @param {string} s encoded value + * @return {string} decoded value + */ + function decode(s) { + if (s) { + s = s.toString().replace(re.pluses, '%20'); + s = decodeURIComponent(s); + } + return s; + } + + /** + * Breaks a uri string down into its individual parts + * @param {string} str uri + * @return {object} parts + */ + function parseUri(str) { + var parser = re.uri_parser; + var parserKeys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "isColonUri", "relative", "path", "directory", "file", "query", "anchor"]; + var m = parser.exec(str || ''); + var parts = {}; + + parserKeys.forEach(function(key, i) { + parts[key] = m[i] || ''; + }); + + return parts; + } + + /** + * Breaks a query string down into an array of key/value pairs + * @param {string} str query + * @return {array} array of arrays (key/value pairs) + */ + function parseQuery(str) { + var i, ps, p, n, k, v, l; + var pairs = []; + + if (typeof(str) === 'undefined' || str === null || str === '') { + return pairs; + } + + if (str.indexOf('?') === 0) { + str = str.substring(1); + } + + ps = str.toString().split(re.query_separator); + + for (i = 0, l = ps.length; i < l; i++) { + p = ps[i]; + n = p.indexOf('='); + + if (n !== 0) { + k = decode(p.substring(0, n)); + v = decode(p.substring(n + 1)); + pairs.push(n === -1 ? [p, null] : [k, v]); + } + + } + return pairs; + } + + /** + * Creates a new Uri object + * @constructor + * @param {string} str + */ + function Uri(str) { + this.uriParts = parseUri(str); + this.queryPairs = parseQuery(this.uriParts.query); + this.hasAuthorityPrefixUserPref = null; + } + + /** + * Define getter/setter methods + */ + ['protocol', 'userInfo', 'host', 'port', 'path', 'anchor'].forEach(function(key) { + Uri.prototype[key] = function(val) { + if (typeof val !== 'undefined') { + this.uriParts[key] = val; + } + return this.uriParts[key]; + }; + }); + + /** + * if there is no protocol, the leading // can be enabled or disabled + * @param {Boolean} val + * @return {Boolean} + */ + Uri.prototype.hasAuthorityPrefix = function(val) { + if (typeof val !== 'undefined') { + this.hasAuthorityPrefixUserPref = val; + } + + if (this.hasAuthorityPrefixUserPref === null) { + return (this.uriParts.source.indexOf('//') !== -1); + } else { + return this.hasAuthorityPrefixUserPref; + } + }; + + Uri.prototype.isColonUri = function (val) { + if (typeof val !== 'undefined') { + this.uriParts.isColonUri = !!val; + } else { + return !!this.uriParts.isColonUri; + } + }; + + /** + * Serializes the internal state of the query pairs + * @param {string} [val] set a new query string + * @return {string} query string + */ + Uri.prototype.query = function(val) { + var s = '', i, param, l; + + if (typeof val !== 'undefined') { + this.queryPairs = parseQuery(val); + } + + for (i = 0, l = this.queryPairs.length; i < l; i++) { + param = this.queryPairs[i]; + if (s.length > 0) { + s += '&'; + } + if (param[1] === null) { + s += param[0]; + } else { + s += param[0]; + s += '='; + if (typeof param[1] !== 'undefined') { + s += encodeURIComponent(param[1]); + } + } + } + return s.length > 0 ? '?' + s : s; + }; + + /** + * returns the first query param value found for the key + * @param {string} key query key + * @return {string} first value found for key + */ + Uri.prototype.getQueryParamValue = function (key) { + var param, i, l; + for (i = 0, l = this.queryPairs.length; i < l; i++) { + param = this.queryPairs[i]; + if (key === param[0]) { + return param[1]; + } + } + }; + + /** + * returns an array of query param values for the key + * @param {string} key query key + * @return {array} array of values + */ + Uri.prototype.getQueryParamValues = function (key) { + var arr = [], i, param, l; + for (i = 0, l = this.queryPairs.length; i < l; i++) { + param = this.queryPairs[i]; + if (key === param[0]) { + arr.push(param[1]); + } + } + return arr; + }; + + /** + * removes query parameters + * @param {string} key remove values for key + * @param {val} [val] remove a specific value, otherwise removes all + * @return {Uri} returns self for fluent chaining + */ + Uri.prototype.deleteQueryParam = function (key, val) { + var arr = [], i, param, keyMatchesFilter, valMatchesFilter, l; + + for (i = 0, l = this.queryPairs.length; i < l; i++) { + + param = this.queryPairs[i]; + keyMatchesFilter = decode(param[0]) === decode(key); + valMatchesFilter = param[1] === val; + + if ((arguments.length === 1 && !keyMatchesFilter) || (arguments.length === 2 && (!keyMatchesFilter || !valMatchesFilter))) { + arr.push(param); + } + } + + this.queryPairs = arr; + + return this; + }; + + /** + * adds a query parameter + * @param {string} key add values for key + * @param {string} val value to add + * @param {integer} [index] specific index to add the value at + * @return {Uri} returns self for fluent chaining + */ + Uri.prototype.addQueryParam = function (key, val, index) { + if (arguments.length === 3 && index !== -1) { + index = Math.min(index, this.queryPairs.length); + this.queryPairs.splice(index, 0, [key, val]); + } else if (arguments.length > 0) { + this.queryPairs.push([key, val]); + } + return this; + }; + + /** + * test for the existence of a query parameter + * @param {string} key check values for key + * @return {Boolean} true if key exists, otherwise false + */ + Uri.prototype.hasQueryParam = function (key) { + var i, len = this.queryPairs.length; + for (i = 0; i < len; i++) { + if (this.queryPairs[i][0] == key) + return true; + } + return false; + }; + + /** + * replaces query param values + * @param {string} key key to replace value for + * @param {string} newVal new value + * @param {string} [oldVal] replace only one specific value (otherwise replaces all) + * @return {Uri} returns self for fluent chaining + */ + Uri.prototype.replaceQueryParam = function (key, newVal, oldVal) { + var index = -1, len = this.queryPairs.length, i, param; + + if (arguments.length === 3) { + for (i = 0; i < len; i++) { + param = this.queryPairs[i]; + if (decode(param[0]) === decode(key) && decodeURIComponent(param[1]) === decode(oldVal)) { + index = i; + break; + } + } + if (index >= 0) { + this.deleteQueryParam(key, decode(oldVal)).addQueryParam(key, newVal, index); + } + } else { + for (i = 0; i < len; i++) { + param = this.queryPairs[i]; + if (decode(param[0]) === decode(key)) { + index = i; + break; + } + } + this.deleteQueryParam(key); + this.addQueryParam(key, newVal, index); + } + return this; + }; + + /** + * Define fluent setter methods (setProtocol, setHasAuthorityPrefix, etc) + */ + ['protocol', 'hasAuthorityPrefix', 'isColonUri', 'userInfo', 'host', 'port', 'path', 'query', 'anchor'].forEach(function(key) { + var method = 'set' + key.charAt(0).toUpperCase() + key.slice(1); + Uri.prototype[method] = function(val) { + this[key](val); + return this; + }; + }); + + /** + * Scheme name, colon and doubleslash, as required + * @return {string} http:// or possibly just // + */ + Uri.prototype.scheme = function() { + var s = ''; + + if (this.protocol()) { + s += this.protocol(); + if (this.protocol().indexOf(':') !== this.protocol().length - 1) { + s += ':'; + } + s += '//'; + } else { + if (this.hasAuthorityPrefix() && this.host()) { + s += '//'; + } + } + + return s; + }; + + /** + * Same as Mozilla nsIURI.prePath + * @return {string} scheme://user:password@host:port + * @see https://developer.mozilla.org/en/nsIURI + */ + Uri.prototype.origin = function() { + var s = this.scheme(); + + if (this.userInfo() && this.host()) { + s += this.userInfo(); + if (this.userInfo().indexOf('@') !== this.userInfo().length - 1) { + s += '@'; + } + } + + if (this.host()) { + s += this.host(); + if (this.port() || (this.path() && this.path().substr(0, 1).match(/[0-9]/))) { + s += ':' + this.port(); + } + } + + return s; + }; + + /** + * Adds a trailing slash to the path + */ + Uri.prototype.addTrailingSlash = function() { + var path = this.path() || ''; + + if (path.substr(-1) !== '/') { + this.path(path + '/'); + } + + return this; + }; + + /** + * Serializes the internal state of the Uri object + * @return {string} + */ + Uri.prototype.toString = function() { + var path, s = this.origin(); + + if (this.isColonUri()) { + if (this.path()) { + s += ':'+this.path(); + } + } else if (this.path()) { + path = this.path(); + if (!(re.ends_with_slashes.test(s) || re.starts_with_slashes.test(path))) { + s += '/'; + } else { + if (s) { + s.replace(re.ends_with_slashes, '/'); + } + path = path.replace(re.starts_with_slashes, '/'); + } + s += path; + } else { + if (this.host() && (this.query().toString() || this.anchor())) { + s += '/'; + } + } + if (this.query().toString()) { + s += this.query().toString(); + } + + if (this.anchor()) { + if (this.anchor().indexOf('#') !== 0) { + s += '#'; + } + s += this.anchor(); + } + + return s; + }; + + /** + * Clone a Uri object + * @return {Uri} duplicate copy of the Uri + */ + Uri.prototype.clone = function() { + return new Uri(this.toString()); + }; + + /** + * export via AMD or CommonJS, otherwise leak a global + */ + if (typeof define === 'function' && define.amd) { + define(function() { + return Uri; + }); + } else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = Uri; + } else { + global.Uri = Uri; + } +}(this)); diff --git a/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js b/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js new file mode 100644 index 000000000..df85886da --- /dev/null +++ b/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js @@ -0,0 +1,137 @@ +function loadScript(url, callback) { + // Adding the script tag to the head as suggested before + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + + // Then bind the event to the callback function. + // There are several events for cross browser compatibility. + script.onreadystatechange = callback; + script.onload = callback; + + // Fire the loading + head.appendChild(script); +} + +function getURLVariable(variable, defaultValue) { + // Find all URL parameters + var query = location.search.substring(1); + var vars = query.split('&'); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split('='); + + // If the query variable parameter is found, decode it to use and return it for use + if (pair[0] === variable) { + return decodeURIComponent(pair[1]); + } + } + return defaultValue || false; +} + +function gbPebble() { + this.configurationURL = null; + this.configurationValues = null; + + this.addEventListener = function(e, f) { + if(e == 'ready') { + this.ready = f; + } + if(e == 'showConfiguration') { + this.showConfiguration = f; + } + if(e == 'webviewclosed') { + this.parseconfig = f; + } + if(e == 'appmessage') { + this.appmessage = f; + } + } + + this.removeEventListener = function(e, f) { + if(e == 'ready') { + this.ready = null; + } + if(e == 'showConfiguration') { + this.showConfiguration = null; + } + if(e == 'webviewclosed') { + this.parseconfig = null; + } + if(e == 'appmessage') { + this.appmessage = null; + } + } + this.actuallyOpenURL = function() { + window.open(this.configurationURL.toString(), "config"); + } + + this.actuallySendData = function() { + GBjs.sendAppMessage(this.configurationValues); + } + + //needs to be called like this because of original Pebble function name + this.openURL = function(url) { + if (url.lastIndexOf("http", 0) === 0) { + document.getElementById("config_url").innerHTML=url; + var UUID = GBjs.getAppUUID(); + this.configurationURL = new Uri(url).addQueryParam("return_to", "gadgetbridge://"+UUID+"?config=true&json="); + } else { + //TODO: add custom return_to + location.href = url; + } + + } + + this.getActiveWatchInfo = function() { + return JSON.parse(GBjs.getActiveWatchInfo()); + } + + this.sendAppMessage = function (dict, callbackAck, callbackNack){ + try { + this.configurationValues = JSON.stringify(dict); + document.getElementById("jsondata").innerHTML=this.configurationValues; + return callbackAck; + } + catch (e) { + GBjs.gbLog("sendAppMessage failed"); + return callbackNack; + } + } + + this.getAccountToken = function() { + return ''; + } + + this.getWatchToken = function() { + return GBjs.getWatchToken(); + } + + this.showSimpleNotificationOnPebble = function(title, body) { + GBjs.gbLog("app wanted to show: " + title + " body: "+ body); + } + + this.ready = function() { + } + +} + +var Pebble = new gbPebble(); + +var jsConfigFile = GBjs.getAppConfigurationFile(); +if (jsConfigFile != null) { + loadScript(jsConfigFile, function() { + if (getURLVariable('config') == 'true') { + document.getElementById('step1').style.display="none"; + var json_string = unescape(getURLVariable('json')); + var t = new Object(); + t.response = json_string; + if (json_string != '') + Pebble.parseconfig(t); + } else { + document.getElementById('step2').style.display="none"; + Pebble.ready(); + Pebble.showConfiguration(); + } + }); +} diff --git a/app/src/main/assets/ic_launcher.svg b/app/src/main/assets/ic_launcher.svg new file mode 100644 index 000000000..03b479948 --- /dev/null +++ b/app/src/main/assets/ic_launcher.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/logback.xml b/app/src/main/assets/logback.xml index f08c0b4c0..07fa3da6b 100644 --- a/app/src/main/assets/logback.xml +++ b/app/src/main/assets/logback.xml @@ -15,15 +15,19 @@ - ${GB_LOGFILES_DIR}/gadgetbridge-%d{yyyy-MM-dd}.log.zip + ${GB_LOGFILES_DIR}/gadgetbridge-%d{yyyy-MM-dd}.%i.log.zip 10 + + + 2MB + - - 5MB - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{1} - %msg%n + + false diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 05c1ac7a9..e17c4584a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -6,11 +6,13 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.res.Resources; import android.os.Build; import android.os.Build.VERSION; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; +import android.util.TypedValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,14 +24,19 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; import nodomain.freeyourgadget.gadgetbridge.database.ActivityDatabaseHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBConstants; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; //import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver; @@ -45,7 +52,13 @@ public class GBApplication extends Application { private static final Lock dbLock = new ReentrantLock(); private static DeviceService deviceService; private static SharedPreferences sharedPrefs; + private static final String PREFS_VERSION = "shared_preferences_version"; + //if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version + private static final int CURRENT_PREFS_VERSION = 2; private static LimitedQueue mIDSenderLookup = new LimitedQueue(16); + private static Appender fileLogger; + private static Prefs prefs; + private static GBPrefs gbPrefs; public static final String ACTION_QUIT = "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.quit"; @@ -79,10 +92,16 @@ public class GBApplication extends Application { super.onCreate(); sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs = new Prefs(sharedPrefs); + gbPrefs = new GBPrefs(prefs); // don't do anything here before we set up logging, otherwise // slf4j may be implicitly initialized before we properly configured it. - setupLogging(); + setupLogging(isFileLoggingEnabled()); + + if (getPrefsFileVersion() != CURRENT_PREFS_VERSION) { + migratePrefs(getPrefsFileVersion()); + } setupExceptionHandler(); // For debugging problems with the logback configuration @@ -111,35 +130,71 @@ public class GBApplication extends Application { } public static boolean isFileLoggingEnabled() { - return sharedPrefs.getBoolean("log_to_file", false); + return prefs.getBoolean("log_to_file", false); } - private void setupLogging() { - if (isFileLoggingEnabled()) { - try { + public static void setupLogging(boolean enable) { + try { + if (fileLogger == null) { File dir = FileUtils.getExternalFilesDir(); // used by assets/logback.xml since the location cannot be statically determined System.setProperty("GB_LOGFILES_DIR", dir.getAbsolutePath()); - getLogger().info("Gadgetbridge version: " + BuildConfig.VERSION_NAME); - } catch (IOException ex) { - Log.e("GBApplication", "External files dir not available, cannot log to file", ex); - removeFileLogger(); + rememberFileLogger(); } - } else { - removeFileLogger(); + if (enable) { + startFileLogger(); + } else { + stopFileLogger(); + } + getLogger().info("Gadgetbridge version: " + BuildConfig.VERSION_NAME); + } catch (IOException ex) { + Log.e("GBApplication", "External files dir not available, cannot log to file", ex); + stopFileLogger(); } } - private void removeFileLogger() { + private static void startFileLogger() { + if (fileLogger != null && !fileLogger.isStarted()) { + addFileLogger(fileLogger); + fileLogger.start(); + } + } + + private static void stopFileLogger() { + if (fileLogger != null && fileLogger.isStarted()) { + fileLogger.stop(); + removeFileLogger(fileLogger); + } + } + + private static void rememberFileLogger() { + ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + fileLogger = root.getAppender("FILE"); + } + + private static void addFileLogger(Appender fileLogger) { try { ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - root.detachAppender("FILE"); + if (!root.isAttached(fileLogger)) { + root.addAppender(fileLogger); + } + } catch (Throwable ex) { + Log.e("GBApplication", "Error adding logger FILE appender", ex); + } + } + + private static void removeFileLogger(Appender fileLogger) { + try { + ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + if (root.isAttached(fileLogger)) { + root.detachAppender(fileLogger); + } } catch (Throwable ex) { Log.e("GBApplication", "Error removing logger FILE appender", ex); } } - private Logger getLogger() { + private static Logger getLogger() { return LoggerFactory.getLogger(GBApplication.class); } @@ -188,10 +243,6 @@ public class GBApplication extends Application { dbLock.unlock(); } - public static boolean isRunningOnKitkatOrLater() { - return VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - } - public static boolean isRunningLollipopOrLater() { return VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } @@ -242,7 +293,84 @@ public class GBApplication extends Application { return result; } + private int getPrefsFileVersion() { + try { + return Integer.parseInt(sharedPrefs.getString(PREFS_VERSION, "0")); //0 is legacy + } catch (Exception e) { + //in version 1 this was an int + return 1; + } + } + + private void migratePrefs(int oldVersion) { + SharedPreferences.Editor editor = sharedPrefs.edit(); + switch (oldVersion) { + case 0: + String legacyGender = sharedPrefs.getString("mi_user_gender", null); + String legacyHeight = sharedPrefs.getString("mi_user_height_cm", null); + String legacyWeigth = sharedPrefs.getString("mi_user_weight_kg", null); + String legacyYOB = sharedPrefs.getString("mi_user_year_of_birth", null); + if (legacyGender != null) { + int gender = "male".equals(legacyGender) ? 1 : "female".equals(legacyGender) ? 0 : 2; + editor.putString(ActivityUser.PREF_USER_GENDER, Integer.toString(gender)); + editor.remove("mi_user_gender"); + } + if (legacyHeight != null) { + editor.putString(ActivityUser.PREF_USER_HEIGHT_CM, legacyHeight); + editor.remove("mi_user_height_cm"); + } + if (legacyWeigth != null) { + editor.putString(ActivityUser.PREF_USER_WEIGHT_KG, legacyWeigth); + editor.remove("mi_user_weight_kg"); + } + if (legacyYOB != null) { + editor.putString(ActivityUser.PREF_USER_YEAR_OF_BIRTH, legacyYOB); + editor.remove("mi_user_year_of_birth"); + } + editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION)); + break; + case 1: + //migrate the integer version of gender introduced in version 1 to a string value, needed for the way Android accesses the shared preferences + int legacyGender_1 = 2; + try { + legacyGender_1 = sharedPrefs.getInt(ActivityUser.PREF_USER_GENDER, 2); + } catch (Exception e) { + Log.e(TAG, "Could not access legacy activity gender", e); + } + editor.putString(ActivityUser.PREF_USER_GENDER, Integer.toString(legacyGender_1)); + //also silently migrate the version to a string value + editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION)); + break; + } + editor.apply(); + } + public static LimitedQueue getIDSenderLookup() { return mIDSenderLookup; } + + public static boolean isDarkThemeEnabled() { + return prefs.getString("pref_key_theme", context.getString(R.string.pref_theme_value_light)).equals(context.getString(R.string.pref_theme_value_dark)); + } + + public static int getTextColor(Context context) { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(android.R.attr.textColor, typedValue, true); + return typedValue.data; + } + public static int getBackgroundColor(Context context) { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(android.R.attr.background, typedValue, true); + return typedValue.data; + } + + public static Prefs getPrefs() { + return prefs; + } + + public static GBPrefs getGBPrefs() { + return gbPrefs; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java new file mode 100644 index 000000000..e501de497 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java @@ -0,0 +1,115 @@ +package nodomain.freeyourgadget.gadgetbridge; + +import android.annotation.TargetApi; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.widget.RemoteViews; +import android.widget.Toast; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms; +import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * Implementation of SleepAlarmWidget functionality. When pressing the widget, an alarm will be set + * to trigger after a predefined number of hours. A toast will confirm the user about this. The + * value is retrieved using ActivityUser.().getActivityUserSleepDuration(). + */ +public class SleepAlarmWidget extends AppWidgetProvider { + + /** + * This is our dedicated action to detect when the widget has been clicked. + */ + public static final String ACTION = + "nodomain.freeyourgadget.gadgetbridge.SLEEP_ALARM_WIDGET_CLICK"; + + static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, + int appWidgetId) { + + // Construct the RemoteViews object + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.sleep_alarm_widget); + + // Add our own click intent + Intent intent = new Intent(ACTION); + PendingIntent clickPI = PendingIntent.getBroadcast( + context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + views.setOnClickPendingIntent(R.id.sleepalarmwidget_text, clickPI); + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // There may be multiple widgets active, so update all of them + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + } + + @Override + public void onEnabled(Context context) { + // Enter relevant functionality for when the first widget is created + } + + @Override + public void onDisabled(Context context) { + // Enter relevant functionality for when the last widget is disabled + } + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + if (ACTION.equals(intent.getAction())) { + int userSleepDuration = new ActivityUser().getActivityUserSleepDuration(); + // current timestamp + GregorianCalendar calendar = new GregorianCalendar(); + // add preferred sleep duration + calendar.add(Calendar.HOUR_OF_DAY, userSleepDuration); + + int hours = calendar.get(calendar.HOUR_OF_DAY); + int minutes = calendar.get(calendar.MINUTE); + + // overwrite the first alarm and activate it + GBAlarm alarm = new GBAlarm(0, true, true, Alarm.ALARM_ONCE, hours, minutes); + alarm.store(); + + if (GBApplication.isRunningLollipopOrLater()) { + setAlarmViaAlarmManager(context, calendar.getTimeInMillis()); + } + + GB.toast(context, + String.format(context.getString(R.string.appwidget_alarms_set), hours, minutes), + Toast.LENGTH_SHORT, GB.INFO); + } + } + + /** + * Use the Android alarm manager to create the alarm icon in the status bar. + * + * @param packageContext {@code Context}: A Context of the application package implementing this + * class. + * @param triggerTime {@code long}: time at which the underlying alarm is triggered in wall time + * milliseconds since the epoch + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void setAlarmViaAlarmManager(Context packageContext, long triggerTime) { + AlarmManager am = (AlarmManager) packageContext.getSystemService(Context.ALARM_SERVICE); + // TODO: launch the alarm configuration activity when clicking the alarm in the status bar + Intent intent = new Intent(packageContext, ConfigureAlarms.class); + PendingIntent pi = PendingIntent.getBroadcast(packageContext, 0, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + am.setAlarmClock(new AlarmManager.AlarmClockInfo(triggerTime, pi), pi); + } +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractGBFragmentActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractGBFragmentActivity.java index b76e50c3a..ba2e5a7f4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractGBFragmentActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractGBFragmentActivity.java @@ -1,7 +1,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities; import android.os.Bundle; -import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; @@ -18,7 +17,7 @@ import android.support.v4.app.FragmentPagerAdapter; * * @see AbstractGBFragment */ -public abstract class AbstractGBFragmentActivity extends FragmentActivity { +public abstract class AbstractGBFragmentActivity extends GBActivity { /** * The {@link android.support.v4.view.PagerAdapter} that will provide * fragments for each of the sections. We use a diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java index d12fee966..965a0cfd6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java @@ -1,16 +1,30 @@ package nodomain.freeyourgadget.gadgetbridge.activities; +import android.content.res.Configuration; import android.os.Bundle; +import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceManager; +import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; import android.support.v4.app.NavUtils; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatDelegate; +import android.support.v7.widget.Toolbar; +import android.text.InputType; +import android.view.MenuInflater; import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; + /** * A settings activity with support for preferences directly displaying their value. * If you combine such preferences with a custom OnPreferenceChangeListener, you have @@ -20,6 +34,7 @@ import org.slf4j.LoggerFactory; public abstract class AbstractSettingsActivity extends PreferenceActivity { private static final Logger LOG = LoggerFactory.getLogger(AbstractSettingsActivity.class); + private AppCompatDelegate delegate; /** * A preference value change listener that updates the preference's summary @@ -28,12 +43,20 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity { private static class SimpleSetSummaryOnChangeListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object value) { + if (preference instanceof EditTextPreference) { + if (((EditTextPreference) preference).getEditText().getKeyListener().getInputType() == InputType.TYPE_CLASS_NUMBER) { + if ("".equals(String.valueOf(value))) { + // reject empty numeric input + return false; + } + } + } updateSummary(preference, value); return true; } public void updateSummary(Preference preference, Object value) { - String stringValue = value.toString(); + String stringValue = String.valueOf(value); if (preference instanceof ListPreference) { // For list preferences, look up the correct display value in @@ -56,15 +79,15 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity { } private static class ExtraSetSummaryOnChangeListener extends SimpleSetSummaryOnChangeListener { - private final Preference.OnPreferenceChangeListener delegate; + private final Preference.OnPreferenceChangeListener prefChangeListener; - public ExtraSetSummaryOnChangeListener(Preference.OnPreferenceChangeListener delegate) { - this.delegate = delegate; + public ExtraSetSummaryOnChangeListener(Preference.OnPreferenceChangeListener prefChangeListener) { + this.prefChangeListener = prefChangeListener; } @Override public boolean onPreferenceChange(Preference preference, Object value) { - boolean result = delegate.onPreferenceChange(preference, value); + boolean result = prefChangeListener.onPreferenceChange(preference, value); if (result) { return super.onPreferenceChange(preference, value); } @@ -74,11 +97,22 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity { private static final SimpleSetSummaryOnChangeListener sBindPreferenceSummaryToValueListener = new SimpleSetSummaryOnChangeListener(); + @Override + protected void onCreate(Bundle savedInstanceState) { + if (GBApplication.isDarkThemeEnabled()) { + setTheme(R.style.GadgetbridgeThemeDark); + } else { + setTheme(R.style.GadgetbridgeTheme); + } + getDelegate().installViewFactory(); + getDelegate().onCreate(savedInstanceState); + super.onCreate(savedInstanceState); + } + @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); - - getActionBar().setDisplayHomeAsUpEnabled(true); + getDelegate().onPostCreate(savedInstanceState); for (String prefKey : getPreferenceKeysWithSummary()) { final Preference pref = findPreference(prefKey); @@ -90,6 +124,67 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity { } } + + @Override + protected void onPostResume() { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override + protected void onTitleChanged(CharSequence title, int color) { + super.onTitleChanged(title, color); + getDelegate().setTitle(title); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + getDelegate().onConfigurationChanged(newConfig); + } + + @Override + protected void onStop() { + super.onStop(); + getDelegate().onStop(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + getDelegate().onDestroy(); + } + + @Override + public MenuInflater getMenuInflater() { + return getDelegate().getMenuInflater(); + } + + @Override + public void setContentView(@LayoutRes int layoutResID) { + getDelegate().setContentView(layoutResID); + } + + @Override + public void setContentView(View view) { + getDelegate().setContentView(view); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().setContentView(view, params); + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().addContentView(view, params); + } + + public void invalidateOptionsMenu() { + getDelegate().invalidateOptionsMenu(); + } + + /** * Subclasses should reimplement this to return the keys of those * preferences which should print its values as a summary below the @@ -141,4 +236,19 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity { } return super.onOptionsItemSelected(item); } + + public ActionBar getSupportActionBar() { + return getDelegate().getSupportActionBar(); + } + + public void setSupportActionBar(@Nullable Toolbar toolbar) { + getDelegate().setSupportActionBar(toolbar); + } + + private AppCompatDelegate getDelegate() { + if (delegate == null) { + delegate = AppCompatDelegate.create(this, null); + } + return delegate; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AlarmDetails.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AlarmDetails.java index a137ad0cb..7b3342ae9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AlarmDetails.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AlarmDetails.java @@ -1,6 +1,5 @@ package nodomain.freeyourgadget.gadgetbridge.activities; -import android.app.Activity; import android.os.Bundle; import android.os.Parcelable; import android.text.format.DateFormat; @@ -12,7 +11,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm; -public class AlarmDetails extends Activity { +public class AlarmDetails extends GBActivity { private GBAlarm alarm; private TimePicker timePicker; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AndroidPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AndroidPairingActivity.java index 284f67915..e7900e444 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AndroidPairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AndroidPairingActivity.java @@ -1,11 +1,10 @@ package nodomain.freeyourgadget.gadgetbridge.activities; -import android.app.Activity; import android.os.Bundle; import nodomain.freeyourgadget.gadgetbridge.R; -public class AndroidPairingActivity extends Activity { +public class AndroidPairingActivity extends GBActivity { @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppBlacklistActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppBlacklistActivity.java index 630e9c047..c7db9fc71 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppBlacklistActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppBlacklistActivity.java @@ -1,15 +1,12 @@ package nodomain.freeyourgadget.gadgetbridge.activities; -import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.v4.app.NavUtils; import android.support.v4.content.LocalBroadcastManager; import android.view.LayoutInflater; @@ -28,13 +25,14 @@ import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; -public class AppBlacklistActivity extends Activity { +public class AppBlacklistActivity extends GBActivity { private static final Logger LOG = LoggerFactory.getLogger(AppBlacklistActivity.class); private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -47,20 +45,41 @@ public class AppBlacklistActivity extends Activity { } }; - private SharedPreferences sharedPrefs; + private IdentityHashMap nameMap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_appblacklist); - getActionBar().setDisplayHomeAsUpEnabled(true); final PackageManager pm = getPackageManager(); - sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); final List packageList = pm.getInstalledApplications(PackageManager.GET_META_DATA); ListView appListView = (ListView) findViewById(R.id.appListView); + // sort the package list by label and blacklist status + nameMap = new IdentityHashMap<>(packageList.size()); + for (ApplicationInfo ai : packageList) { + CharSequence name = pm.getApplicationLabel(ai); + if (name == null) { + name = ai.packageName; + } + if (GBApplication.blacklist.contains(ai.packageName)) { + // sort blacklisted first by prefixing with a '!' + name = "!" + name; + } + nameMap.put(ai, name.toString()); + } + + Collections.sort(packageList, new Comparator() { + @Override + public int compare(ApplicationInfo ai1, ApplicationInfo ai2) { + final String s1 = nameMap.get(ai1); + final String s2 = nameMap.get(ai2); + return s1.compareTo(s2); + } + }); + final ArrayAdapter adapter = new ArrayAdapter(this, R.layout.item_with_checkbox, packageList) { @Override public View getView(int position, View view, ViewGroup parent) { @@ -76,27 +95,11 @@ public class AppBlacklistActivity extends Activity { CheckBox checkbox = (CheckBox) view.findViewById(R.id.item_checkbox); deviceAppVersionAuthorLabel.setText(appInfo.packageName); - deviceAppNameLabel.setText(appInfo.loadLabel(pm)); + deviceAppNameLabel.setText(nameMap.get(appInfo)); deviceImageView.setImageDrawable(appInfo.loadIcon(pm)); checkbox.setChecked(GBApplication.blacklist.contains(appInfo.packageName)); - Collections.sort(packageList, new Comparator() { - @Override - public int compare(ApplicationInfo ai1, ApplicationInfo ai2) { - boolean blacklisted1 = GBApplication.blacklist.contains(ai1.packageName); - boolean blacklisted2 = GBApplication.blacklist.contains(ai2.packageName); - - if ((blacklisted1 && blacklisted2) || (!blacklisted1 && !blacklisted2)) { - // both blacklisted or both not blacklisted = sort by alphabet - return ai1.packageName.compareTo(ai2.packageName); - } else if (blacklisted1) { - return -1; - } else { - return 1; - } - } - }); return view; } }; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java index 172f5285c..2e6bbba04 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java @@ -1,14 +1,11 @@ package nodomain.freeyourgadget.gadgetbridge.activities; -import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.v4.app.NavUtils; import android.support.v4.content.LocalBroadcastManager; import android.view.ContextMenu; @@ -30,12 +27,15 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; +import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -public class AppManagerActivity extends Activity { +public class AppManagerActivity extends GBActivity { public static final String ACTION_REFRESH_APPLIST = "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist"; private static final Logger LOG = LoggerFactory.getLogger(AppManagerActivity.class); @@ -58,7 +58,7 @@ public class AppManagerActivity extends Activity { appList.add(new GBDeviceApp(uuid, appName, appCreator, "", appType)); } - if (sharedPrefs.getBoolean("pebble_force_untested", false)) { + if (prefs.getBoolean("pebble_force_untested", false)) { appList.addAll(getSystemApps()); } @@ -67,17 +67,20 @@ public class AppManagerActivity extends Activity { } }; - private SharedPreferences sharedPrefs; + private Prefs prefs; private final List appList = new ArrayList<>(); private GBDeviceAppAdapter mGBDeviceAppAdapter; private GBDeviceApp selectedApp = null; + private GBDevice mGBDevice = null; private List getSystemApps() { List 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(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); + if (mGBDevice != null && !"aplite".equals(PebbleUtils.getPlatformName(mGBDevice.getHardwareVersion()))) { + systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); + } return systemApps; } @@ -97,11 +100,14 @@ public class AppManagerActivity extends Activity { for (File file : files) { if (file.getName().endsWith(".pbw")) { String baseName = file.getName().substring(0, file.getName().length() - 4); + //metadata File jsonFile = new File(cachePath, baseName + ".json"); + //configuration + File configFile = new File(cachePath, baseName + "_config.js"); try { String jsonstring = FileUtils.getStringFromFile(jsonFile); JSONObject json = new JSONObject(jsonstring); - cachedAppList.add(new GBDeviceApp(json)); + cachedAppList.add(new GBDeviceApp(json, configFile.exists())); } catch (Exception e) { LOG.warn("could not read json file for " + baseName, e.getMessage(), e); cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), baseName, "N/A", "", GBDeviceApp.Type.UNKNOWN)); @@ -116,10 +122,16 @@ public class AppManagerActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + Bundle extras = getIntent().getExtras(); + if (extras != null) { + mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE); + } else { + throw new IllegalArgumentException("Must provide a device when invoking this activity"); + } + + prefs = GBApplication.getPrefs(); setContentView(R.layout.activity_appmanager); - getActionBar().setDisplayHomeAsUpEnabled(true); ListView appListView = (ListView) findViewById(R.id.appListView); mGBDeviceAppAdapter = new GBDeviceAppAdapter(this, appList); @@ -137,7 +149,7 @@ public class AppManagerActivity extends Activity { appList.addAll(getCachedApps()); - if (sharedPrefs.getBoolean("pebble_force_untested", false)) { + if (prefs.getBoolean("pebble_force_untested", false)) { appList.addAll(getSystemApps()); } @@ -157,24 +169,29 @@ public class AppManagerActivity extends Activity { AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo; selectedApp = appList.get(acmi.position); - if (!selectedApp.isInCache() && !PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) { + if (!selectedApp.isInCache()) { menu.removeItem(R.id.appmanager_app_reinstall); } + if (!PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) { + menu.removeItem(R.id.appmanager_health_activate); + menu.removeItem(R.id.appmanager_health_deactivate); + } else if (PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) { + menu.removeItem(R.id.appmanager_app_delete); + } + if (!selectedApp.isConfigurable()) { + menu.removeItem(R.id.appmanager_app_configure); + } menu.setHeaderTitle(selectedApp.getName()); } @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { + case R.id.appmanager_health_deactivate: case R.id.appmanager_app_delete: GBApplication.deviceService().onAppDelete(selectedApp.getUUID()); return true; case R.id.appmanager_app_reinstall: - if (PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) { - GBApplication.deviceService().onInstallApp(Uri.parse("fake://health")); - return true; - } - File cachePath; try { cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID() + ".pbw"); @@ -184,6 +201,17 @@ public class AppManagerActivity extends Activity { } GBApplication.deviceService().onInstallApp(Uri.fromFile(cachePath)); return true; + case R.id.appmanager_health_activate: + GBApplication.deviceService().onInstallApp(Uri.parse("fake://health")); + return true; + case R.id.appmanager_app_configure: + GBApplication.deviceService().onAppStart(selectedApp.getUUID(), true); + + Intent startIntent = new Intent(getApplicationContext(), ExternalPebbleJSActivity.class); + startIntent.putExtra("app_uuid", selectedApp.getUUID()); + startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice); + startActivity(startIntent); + return true; default: return super.onContextItemSelected(item); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java index a640faff2..0a978d5d3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java @@ -1,11 +1,9 @@ package nodomain.freeyourgadget.gadgetbridge.activities; -import android.app.ListActivity; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; import android.view.MenuItem; +import android.widget.ListView; import java.util.Arrays; import java.util.HashSet; @@ -16,11 +14,12 @@ import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.adapter.GBAlarmListAdapter; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ALARMS; -public class ConfigureAlarms extends ListActivity { +public class ConfigureAlarms extends GBActivity { private static final int REQ_CONFIGURE_ALARM = 1; @@ -33,19 +32,19 @@ public class ConfigureAlarms extends ListActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_configure_alarms); - getActionBar().setDisplayHomeAsUpEnabled(true); - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - preferencesAlarmListSet = sharedPrefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet()); + Prefs prefs = GBApplication.getPrefs(); + preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet()); if (preferencesAlarmListSet.isEmpty()) { //initialize the preferences preferencesAlarmListSet = new HashSet<>(Arrays.asList(GBAlarm.DEFAULT_ALARMS)); - sharedPrefs.edit().putStringSet(PREF_MIBAND_ALARMS, preferencesAlarmListSet).apply(); + prefs.getPreferences().edit().putStringSet(PREF_MIBAND_ALARMS, preferencesAlarmListSet).apply(); } mGBAlarmListAdapter = new GBAlarmListAdapter(this, preferencesAlarmListSet); - setListAdapter(mGBAlarmListAdapter); + ListView listView = (ListView) findViewById(R.id.alarm_list); + listView.setAdapter(mGBAlarmListAdapter); updateAlarmsFromPrefs(); } @@ -66,9 +65,9 @@ public class ConfigureAlarms extends ListActivity { } private void updateAlarmsFromPrefs() { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - preferencesAlarmListSet = sharedPrefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet()); - int reservedSlots = Integer.parseInt(sharedPrefs.getString(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, "0")); + Prefs prefs = GBApplication.getPrefs(); + preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet()); + int reservedSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0); mGBAlarmListAdapter.setAlarmList(preferencesAlarmListSet, reservedSlots); mGBAlarmListAdapter.notifyDataSetChanged(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java index db241ed99..702aa1346 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java @@ -1,5 +1,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities; +import android.Manifest; +import android.annotation.TargetApi; import android.app.Activity; import android.app.ProgressDialog; import android.bluetooth.BluetoothDevice; @@ -8,9 +10,12 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceManager; +import android.support.design.widget.FloatingActionButton; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.widget.SwipeRefreshLayout; import android.view.ContextMenu; @@ -18,6 +23,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; +import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; @@ -29,6 +35,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import de.cketti.library.changelog.ChangeLog; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity; @@ -37,8 +44,9 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -public class ControlCenter extends Activity { +public class ControlCenter extends GBActivity { private static final Logger LOG = LoggerFactory.getLogger(ControlCenter.class); @@ -46,6 +54,9 @@ public class ControlCenter extends Activity { = "nodomain.freeyourgadget.gadgetbridge.controlcenter.action.set_version"; private TextView hintTextView; + private FloatingActionButton fab; + private ImageView background; + private SwipeRefreshLayout swipeLayout; private GBDeviceAdapter mGBDeviceAdapter; private GBDevice selectedDevice = null; @@ -116,15 +127,26 @@ public class ControlCenter extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_controlcenter); + hintTextView = (TextView) findViewById(R.id.hintTextView); ListView deviceListView = (ListView) findViewById(R.id.deviceListView); + fab = (FloatingActionButton) findViewById(R.id.fab); + background = (ImageView) findViewById(R.id.no_items_bg); + + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + launchDiscoveryActivity(); + } + }); + mGBDeviceAdapter = new GBDeviceAdapter(this, deviceList); deviceListView.setAdapter(this.mGBDeviceAdapter); deviceListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View v, int position, long id) { GBDevice gbDevice = deviceList.get(position); - if (gbDevice.isConnected()) { + if (gbDevice.isInitialized()) { DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); Class primaryActivity = coordinator.getPrimaryActivity(); if (primaryActivity != null) { @@ -161,17 +183,25 @@ public class ControlCenter extends Activity { /* * Ask for permission to intercept notifications on first run. */ - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - if (sharedPrefs.getBoolean("firstrun", true)) { - sharedPrefs.edit().putBoolean("firstrun", false).apply(); + Prefs prefs = GBApplication.getPrefs(); + if (prefs.getBoolean("firstrun", true)) { + prefs.getPreferences().edit().putBoolean("firstrun", false).apply(); Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); startActivity(enableIntent); } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + checkAndRequestPermissions(); + } + + ChangeLog cl = new ChangeLog(this); + if (cl.isFirstRun()) { + cl.getLogDialog().show(); + } + GBApplication.deviceService().start(); enableSwipeRefresh(selectedDevice); - if (GB.isBluetoothEnabled() && deviceList.isEmpty()) { - // start discovery when no devices are present + if (GB.isBluetoothEnabled() && deviceList.isEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { startActivity(new Intent(this, DiscoveryActivity.class)); } else { GBApplication.deviceService().requestDeviceInfo(); @@ -196,6 +226,9 @@ public class ControlCenter extends Activity { if (!coordinator.supportsScreenshots()) { menu.removeItem(R.id.controlcenter_take_screenshot); } + if (!coordinator.supportsAlarmConfiguration()) { + menu.removeItem(R.id.controlcenter_configure_alarms); + } if (selectedDevice.getState() == GBDevice.State.NOT_CONNECTED) { menu.removeItem(R.id.controlcenter_disconnect); @@ -314,15 +347,19 @@ public class ControlCenter extends Activity { Intent quitIntent = new Intent(GBApplication.ACTION_QUIT); LocalBroadcastManager.getInstance(this).sendBroadcast(quitIntent); return true; - case R.id.action_discover: - Intent discoverIntent = new Intent(this, DiscoveryActivity.class); - startActivity(discoverIntent); - return true; } return super.onOptionsItemSelected(item); } + private void launchDiscoveryActivity() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + startActivity(new Intent(android.provider.Settings.ACTION_BLUETOOTH_SETTINGS)); + } else { + startActivity(new Intent(this, DiscoveryActivity.class)); + } + } + @Override protected void onDestroy() { LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); @@ -346,12 +383,52 @@ public class ControlCenter extends Activity { } } + if (deviceList.isEmpty()) { + background.setVisibility(View.VISIBLE); + } else { + background.setVisibility(View.INVISIBLE); + } + if (connected) { - hintTextView.setText(R.string.tap_connected_device_for_app_mananger); + DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(selectedDevice); + hintTextView.setText(coordinator.getTapString()); } else if (!deviceList.isEmpty()) { hintTextView.setText(R.string.tap_a_device_to_connect); } mGBDeviceAdapter.notifyDataSetChanged(); } + + @TargetApi(Build.VERSION_CODES.M) + private void checkAndRequestPermissions() { + List wantedPermissions = new ArrayList<>(); + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED) + wantedPermissions.add(Manifest.permission.BLUETOOTH); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_DENIED) + wantedPermissions.add(Manifest.permission.BLUETOOTH_ADMIN); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED) + wantedPermissions.add(Manifest.permission.READ_CONTACTS); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_DENIED) + wantedPermissions.add(Manifest.permission.CALL_PHONE); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED) + wantedPermissions.add(Manifest.permission.READ_PHONE_STATE); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.PROCESS_OUTGOING_CALLS) == PackageManager.PERMISSION_DENIED) + wantedPermissions.add(Manifest.permission.PROCESS_OUTGOING_CALLS); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_DENIED) + wantedPermissions.add(Manifest.permission.READ_SMS); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_DENIED) + wantedPermissions.add(Manifest.permission.SEND_SMS); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) + wantedPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_DENIED) + wantedPermissions.add(Manifest.permission.READ_CALENDAR); + if (ContextCompat.checkSelfPermission(this, "com.fsck.k9.permission.READ_MESSAGES") == PackageManager.PERMISSION_DENIED) + wantedPermissions.add("com.fsck.k9.permission.READ_MESSAGES"); + + if (!wantedPermissions.isEmpty()) + ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[wantedPermissions.size()]), 0); + } + + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java index a4fbe772c..d7d81f692 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java @@ -1,6 +1,5 @@ package nodomain.freeyourgadget.gadgetbridge.activities; -import android.app.Activity; import android.app.AlertDialog; import android.app.NotificationManager; import android.app.PendingIntent; @@ -14,6 +13,7 @@ import android.os.Bundle; import android.support.v4.app.NavUtils; import android.support.v4.app.NotificationCompat; import android.support.v4.app.RemoteInput; +import android.support.v4.content.LocalBroadcastManager; import android.view.MenuItem; import android.view.View; import android.widget.Button; @@ -29,14 +29,16 @@ 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.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; -import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; -public class DebugActivity extends Activity { +public class DebugActivity extends GBActivity { private static final Logger LOG = LoggerFactory.getLogger(DebugActivity.class); private static final String EXTRA_REPLY = "reply"; @@ -53,6 +55,7 @@ public class DebugActivity extends Activity { private Button setMusicInfoButton; private Button setTimeButton; private Button rebootButton; + private Button HeartRateButton; private Button exportDBButton; private Button importDBButton; private Button deleteDBButton; @@ -62,15 +65,22 @@ public class DebugActivity extends Activity { @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { - case GBApplication.ACTION_QUIT: + case GBApplication.ACTION_QUIT: { finish(); break; - case ACTION_REPLY: + } + case ACTION_REPLY: { Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); CharSequence reply = remoteInput.getCharSequence(EXTRA_REPLY); LOG.info("got wearable reply: " + reply); GB.toast(context, "got wearable reply: " + reply, Toast.LENGTH_SHORT, GB.INFO); break; + } + case DeviceService.ACTION_HEARTRATE_MEASUREMENT: { + int hrValue = intent.getIntExtra(DeviceService.EXTRA_HEART_RATE_VALUE, -1); + GB.toast(DebugActivity.this, "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO); + break; + } } } }; @@ -79,12 +89,13 @@ public class DebugActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_debug); - getActionBar().setDisplayHomeAsUpEnabled(true); IntentFilter filter = new IntentFilter(); filter.addAction(GBApplication.ACTION_QUIT); filter.addAction(ACTION_REPLY); - registerReceiver(mReceiver, filter); + filter.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT); + LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter); + registerReceiver(mReceiver, filter); // for ACTION_REPLY editContent = (EditText) findViewById(R.id.editContent); sendSMSButton = (Button) findViewById(R.id.sendSMSButton); @@ -117,20 +128,20 @@ public class DebugActivity extends Activity { incomingCallButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - GBApplication.deviceService().onSetCallState( - editContent.getText().toString(), - null, - ServiceCommand.CALL_INCOMING); + CallSpec callSpec = new CallSpec(); + callSpec.command = CallSpec.CALL_INCOMING; + callSpec.number = editContent.getText().toString(); + GBApplication.deviceService().onSetCallState(callSpec); } }); outgoingCallButton = (Button) findViewById(R.id.outgoingCallButton); outgoingCallButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - GBApplication.deviceService().onSetCallState( - editContent.getText().toString(), - null, - ServiceCommand.CALL_OUTGOING); + CallSpec callSpec = new CallSpec(); + callSpec.command = CallSpec.CALL_OUTGOING; + callSpec.number = editContent.getText().toString(); + GBApplication.deviceService().onSetCallState(callSpec); } }); @@ -138,20 +149,18 @@ public class DebugActivity extends Activity { startCallButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - GBApplication.deviceService().onSetCallState( - null, - null, - ServiceCommand.CALL_START); + CallSpec callSpec = new CallSpec(); + callSpec.command = CallSpec.CALL_START; + GBApplication.deviceService().onSetCallState(callSpec); } }); endCallButton = (Button) findViewById(R.id.endCallButton); endCallButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - GBApplication.deviceService().onSetCallState( - null, - null, - ServiceCommand.CALL_END); + CallSpec callSpec = new CallSpec(); + callSpec.command = CallSpec.CALL_END; + GBApplication.deviceService().onSetCallState(callSpec); } }); @@ -185,15 +194,28 @@ public class DebugActivity extends Activity { GBApplication.deviceService().onReboot(); } }); + HeartRateButton = (Button) findViewById(R.id.HearRateButton); + HeartRateButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + GB.toast("Measuring heart rate, please wait...", Toast.LENGTH_LONG, GB.INFO); + GBApplication.deviceService().onHeartRateTest(); + } + }); setMusicInfoButton = (Button) findViewById(R.id.setMusicInfoButton); setMusicInfoButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - GBApplication.deviceService().onSetMusicInfo( - editContent.getText().toString() + "(artist)", - editContent.getText().toString() + "(album)", - editContent.getText().toString() + "(tracl)"); + MusicSpec musicSpec = new MusicSpec(); + musicSpec.artist = editContent.getText().toString() + "(artist)"; + musicSpec.album = editContent.getText().toString() + "(album)"; + musicSpec.track = editContent.getText().toString() + "(track)"; + musicSpec.duration = 10; + musicSpec.trackCount = 5; + musicSpec.trackNr = 2; + + GBApplication.deviceService().onSetMusicInfo(musicSpec); } }); @@ -337,6 +359,7 @@ public class DebugActivity extends Activity { @Override protected void onDestroy() { super.onDestroy(); + LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); unregisterReceiver(mReceiver); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java index 5b0d5549f..f2ab531b6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java @@ -32,7 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; -public class DiscoveryActivity extends Activity implements AdapterView.OnItemClickListener { +public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemClickListener { private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivity.class); private static final long SCAN_DURATION = 60000; // 60s @@ -249,7 +249,13 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli private void bluetoothStateChanged(int oldState, int newState) { discoveryFinished(); - startButton.setEnabled(newState == BluetoothAdapter.STATE_ON); + if (newState == BluetoothAdapter.STATE_ON) { + this.adapter = BluetoothAdapter.getDefaultAdapter(); + startButton.setEnabled(true); + } else { + this.adapter = null; + startButton.setEnabled(false); + } } private void discoveryFinished() { @@ -284,8 +290,15 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli return false; } BluetoothAdapter adapter = bluetoothService.getAdapter(); + if (adapter == null) { + LOG.warn("No bluetooth available"); + this.adapter = null; + return false; + } if (!adapter.isEnabled()) { LOG.warn("Bluetooth not enabled"); + Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivity(enableBtIntent); this.adapter = null; return false; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java new file mode 100644 index 000000000..7dba008be --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java @@ -0,0 +1,216 @@ +package nodomain.freeyourgadget.gadgetbridge.activities; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.util.Log; +import android.view.MenuItem; +import android.webkit.ConsoleMessage; +import android.webkit.JavascriptInterface; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Toast; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.Iterator; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; + +public class ExternalPebbleJSActivity extends GBActivity { + + private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class); + + private UUID appUuid; + private GBDevice mGBDevice = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE); + } else { + throw new IllegalArgumentException("Must provide a device when invoking this activity"); + } + + String queryString = ""; + Uri uri = getIntent().getData(); + if (uri != null) { + //getting back with configuration data + appUuid = UUID.fromString(uri.getHost()); + queryString = uri.getEncodedQuery(); + } else { + appUuid = (UUID) getIntent().getSerializableExtra("app_uuid"); + } + + setContentView(R.layout.activity_external_pebble_js); + + WebView myWebView = (WebView) findViewById(R.id.configureWebview); + myWebView.clearCache(true); + myWebView.setWebViewClient(new GBWebClient()); + myWebView.setWebChromeClient(new GBChromeClient()); + WebSettings webSettings = myWebView.getSettings(); + webSettings.setJavaScriptEnabled(true); + //needed to access the DOM + webSettings.setDomStorageEnabled(true); + + JSInterface gbJSInterface = new JSInterface(); + myWebView.addJavascriptInterface(gbJSInterface, "GBjs"); + + myWebView.loadUrl("file:///android_asset/app_config/configure.html?" + queryString); + } + + private JSONObject getAppConfigurationKeys() { + try { + File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache"); + File configurationFile = new File(destDir, appUuid.toString() + ".json"); + if (configurationFile.exists()) { + String jsonstring = FileUtils.getStringFromFile(configurationFile); + JSONObject json = new JSONObject(jsonstring); + return json.getJSONObject("appKeys"); + } + } catch (IOException | JSONException e) { + e.printStackTrace(); + } + return null; + } + + private class GBChromeClient extends WebChromeClient { + @Override + public boolean onConsoleMessage(ConsoleMessage consoleMessage) { + if (ConsoleMessage.MessageLevel.ERROR.equals(consoleMessage.messageLevel())) { + GB.toast(consoleMessage.message(), Toast.LENGTH_LONG, GB.ERROR); + //TODO: show error page + } + return super.onConsoleMessage(consoleMessage); + } + + } + + private class GBWebClient extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (url.startsWith("http://") || url.startsWith("https://")) { + Intent i = new Intent(Intent.ACTION_VIEW, + Uri.parse(url)); + startActivity(i); + } else { + url = url.replaceFirst("^pebblejs://close#", "file:///android_asset/app_config/configure.html?config=true&json="); + view.loadUrl(url); + } + + return true; + + } + } + + private class JSInterface { + + public JSInterface() { + } + + @JavascriptInterface + public void gbLog(String msg) { + Log.d("WEBVIEW", msg); + } + + @JavascriptInterface + public void sendAppMessage(String msg) { + LOG.debug("from WEBVIEW: ", msg); + JSONObject knownKeys = getAppConfigurationKeys(); + + try { + JSONObject in = new JSONObject(msg); + JSONObject out = new JSONObject(); + String cur_key; + for (Iterator key = in.keys(); key.hasNext(); ) { + cur_key = key.next(); + int pebbleAppIndex = knownKeys.optInt(cur_key); + if (pebbleAppIndex != 0) { + Object obj = in.get(cur_key); + if (obj instanceof Boolean) { + obj = ((Boolean) obj) ? "true" : "false"; + } + out.put(String.valueOf(pebbleAppIndex), obj); + } else { + GB.toast("Discarded key " + cur_key + ", not found in the local configuration.", Toast.LENGTH_SHORT, GB.WARN); + } + } + LOG.info(out.toString()); + GBApplication.deviceService().onAppConfiguration(appUuid, out.toString()); + + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @JavascriptInterface + public String getActiveWatchInfo() { + JSONObject wi = new JSONObject(); + try { + wi.put("firmware", mGBDevice.getFirmwareVersion()); + wi.put("platform", PebbleUtils.getPlatformName(mGBDevice.getHardwareVersion())); + wi.put("model", PebbleUtils.getModel(mGBDevice.getHardwareVersion())); + //TODO: use real info + wi.put("language", "en"); + } catch (JSONException e) { + e.printStackTrace(); + } + //Json not supported apparently, we need to cast back and forth + return wi.toString(); + } + + @JavascriptInterface + public String getAppConfigurationFile() { + try { + File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache"); + File configurationFile = new File(destDir, appUuid.toString() + "_config.js"); + if (configurationFile.exists()) { + return "file:///" + configurationFile.getAbsolutePath(); + } + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + @JavascriptInterface + public String getAppUUID() { + return appUuid.toString(); + } + + @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/ + return "gb" + appUuid.toString(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java index 0c7b88cdf..90390a1af 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java @@ -1,6 +1,5 @@ package nodomain.freeyourgadget.gadgetbridge.activities; -import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -29,14 +28,16 @@ import nodomain.freeyourgadget.gadgetbridge.adapter.ItemWithDetailsAdapter; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; -public class FwAppInstallerActivity extends Activity implements InstallActivity { +public class FwAppInstallerActivity extends GBActivity implements InstallActivity { private static final Logger LOG = LoggerFactory.getLogger(FwAppInstallerActivity.class); + private static final String ITEM_DETAILS = "details"; private TextView fwAppInstallTextView; private Button installButton; @@ -45,13 +46,22 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity private InstallHandler installHandler; private boolean mayConnect; + private ProgressBar mProgressBar; + private ListView itemListView; + private final List mItems = new ArrayList<>(); + private ItemWithDetailsAdapter mItemAdapter; + + private ListView detailsListView; + private ItemWithDetailsAdapter mDetailsItemAdapter; + private ArrayList mDetails = new ArrayList<>(); + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (action.equals(GBApplication.ACTION_QUIT)) { + if (GBApplication.ACTION_QUIT.equals(action)) { finish(); - } else if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) { + } else if (GBDevice.ACTION_DEVICE_CHANGED.equals(action)) { device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); if (device != null) { refreshBusyState(device); @@ -67,13 +77,13 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity validateInstallation(); } } + } else if (GB.ACTION_DISPLAY_MESSAGE.equals(action)) { + String message = intent.getStringExtra(GB.DISPLAY_MESSAGE_MESSAGE); + int severity = intent.getIntExtra(GB.DISPLAY_MESSAGE_SEVERITY, GB.INFO); + addMessage(message, severity); } } }; - private ProgressBar mProgressBar; - private ListView itemListView; - private final List mItems = new ArrayList<>(); - private ItemWithDetailsAdapter mItemAdapter; private void refreshBusyState(GBDevice dev) { if (dev.isConnecting() || dev.isBusy()) { @@ -102,11 +112,18 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_appinstaller); - getActionBar().setDisplayHomeAsUpEnabled(true); + GBDevice dev = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE); if (dev != null) { device = dev; } + if (savedInstanceState != null) { + mDetails = savedInstanceState.getParcelableArrayList(ITEM_DETAILS); + if (mDetails == null) { + mDetails = new ArrayList<>(); + } + } + mayConnect = true; itemListView = (ListView) findViewById(R.id.itemListView); mItemAdapter = new ItemWithDetailsAdapter(this, mItems); @@ -114,10 +131,15 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity fwAppInstallTextView = (TextView) findViewById(R.id.infoTextView); installButton = (Button) findViewById(R.id.installButton); mProgressBar = (ProgressBar) findViewById(R.id.installProgressBar); + detailsListView = (ListView) findViewById(R.id.detailsListView); + mDetailsItemAdapter = new ItemWithDetailsAdapter(this, mDetails); + mDetailsItemAdapter.setSize(ItemWithDetailsAdapter.SIZE_SMALL); + detailsListView.setAdapter(mDetailsItemAdapter); setInstallEnabled(false); IntentFilter filter = new IntentFilter(); filter.addAction(GBApplication.ACTION_QUIT); filter.addAction(GBDevice.ACTION_DEVICE_CHANGED); + filter.addAction(GB.ACTION_DISPLAY_MESSAGE); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter); installButton.setOnClickListener(new View.OnClickListener() { @@ -145,6 +167,12 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity } } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelableArrayList(ITEM_DETAILS, mDetails); + } + private InstallHandler findInstallHandlerFor(Uri uri) { for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) { InstallHandler handler = coordinator.findInstallHandler(uri, this); @@ -195,4 +223,9 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity mItems.add(item); mItemAdapter.notifyDataSetChanged(); } + + private void addMessage(String message, int severity) { + mDetails.add(new GenericItem(message)); + mDetailsItemAdapter.notifyDataSetChanged(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/GBActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/GBActivity.java new file mode 100644 index 000000000..39c972ea1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/GBActivity.java @@ -0,0 +1,22 @@ +package nodomain.freeyourgadget.gadgetbridge.activities; + + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; + + +public class GBActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + if (GBApplication.isDarkThemeEnabled()) { + setTheme(R.style.GadgetbridgeThemeDark); + } else { + setTheme(R.style.GadgetbridgeTheme); + } + + super.onCreate(savedInstanceState); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java new file mode 100644 index 000000000..a4d39925d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java @@ -0,0 +1,14 @@ +package nodomain.freeyourgadget.gadgetbridge.activities; + +public class HeartRateUtils { + public static final int MAX_HEART_RATE_VALUE = 250; + public static final int MIN_HEART_RATE_VALUE = 0; + /** + * The maxiumum gap between two hr measurements in which + * we interpolate between the measurements. Otherwise, two + * distinct measurements will be shown. + * + * Value is in minutes + */ + public static final int MAX_HR_MEASUREMENTS_GAP_MINUTES = 10; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java index 5150f0825..c8bfe8356 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java @@ -7,11 +7,22 @@ import android.os.Bundle; import android.preference.ListPreference; import android.preference.Preference; import android.support.v4.content.LocalBroadcastManager; +import android.widget.Toast; +import java.io.IOException; import java.util.List; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_GENDER; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_HEIGHT_CM; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_SLEEP_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_WEIGHT_KG; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_YEAR_OF_BIRTH; public class SettingsActivity extends AbstractSettingsActivity { @Override @@ -74,6 +85,28 @@ public class SettingsActivity extends AbstractSettingsActivity { }); + pref = findPreference("log_to_file"); + pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + boolean doEnable = Boolean.TRUE.equals(newVal); + try { + if (doEnable) { + FileUtils.getExternalFilesDir(); // ensures that it is created + } + GBApplication.setupLogging(doEnable); + } catch (IOException ex) { + GB.toast(getApplicationContext(), + getString(R.string.error_creating_directory_for_logfiles, ex.getLocalizedMessage()), + Toast.LENGTH_LONG, + GB.ERROR, + ex); + } + return true; + } + + }); + // Get all receivers of Media Buttons Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); @@ -103,10 +136,6 @@ public class SettingsActivity extends AbstractSettingsActivity { @Override protected String[] getPreferenceKeysWithSummary() { return new String[]{ - "audio_player", - "notification_mode_calls", - "notification_mode_sms", - "notification_mode_k9mail", "pebble_emu_addr", "pebble_emu_port", "pebble_reconnect_attempts", @@ -127,6 +156,10 @@ public class SettingsActivity extends AbstractSettingsActivity { "canned_reply_14", "canned_reply_15", "canned_reply_16", + PREF_USER_YEAR_OF_BIRTH, + PREF_USER_HEIGHT_CM, + PREF_USER_WEIGHT_KG, + PREF_USER_SLEEP_DURATION, }; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java index 61afd2792..b4c776eef 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; @@ -12,9 +13,15 @@ import android.view.View; import com.github.mikephil.charting.charts.BarLineChartBase; import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.data.BarData; import com.github.mikephil.charting.data.BarDataSet; import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.CombinedData; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,14 +30,17 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.List; import java.util.Set; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; +import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.database.DBAccess; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; @@ -72,6 +82,8 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } }; private boolean mChartDirty = true; + private boolean supportsHeartrateChart = true; + private AsyncTask refreshTask; public boolean isChartDirty() { return mChartDirty; @@ -79,6 +91,10 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { public abstract String getTitle(); + public boolean supportsHeartrate() { + return supportsHeartrateChart; + } + protected static final class ActivityConfig { public final int type; public final String label; @@ -101,11 +117,15 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { protected int DESCRIPTION_COLOR; protected int CHART_TEXT_COLOR; protected int LEGEND_TEXT_COLOR; + protected int HEARTRATE_COLOR; + protected int HEARTRATE_FILL_COLOR; protected int AK_ACTIVITY_COLOR; protected int AK_DEEP_SLEEP_COLOR; protected int AK_LIGHT_SLEEP_COLOR; protected int AK_NOT_WORN_COLOR; + protected String HEARTRATE_LABEL; + protected AbstractChartFragment(String... intentFilterActions) { mIntentFilterActions = new HashSet<>(); if (intentFilterActions != null) { @@ -130,15 +150,18 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } protected void init() { - BACKGROUND_COLOR = getResources().getColor(R.color.background_material_light); - DESCRIPTION_COLOR = getResources().getColor(R.color.primarytext); + BACKGROUND_COLOR = GBApplication.getBackgroundColor(getContext()); + LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(getContext()); CHART_TEXT_COLOR = getResources().getColor(R.color.secondarytext); - LEGEND_TEXT_COLOR = getResources().getColor(R.color.primarytext); + HEARTRATE_COLOR = getResources().getColor(R.color.chart_heartrate); + HEARTRATE_FILL_COLOR = getResources().getColor(R.color.chart_heartrate_fill); AK_ACTIVITY_COLOR = getResources().getColor(R.color.chart_activity_light); AK_DEEP_SLEEP_COLOR = getResources().getColor(R.color.chart_light_sleep_light); AK_LIGHT_SLEEP_COLOR = getResources().getColor(R.color.chart_deep_sleep_light); AK_NOT_WORN_COLOR = getResources().getColor(R.color.chart_not_worn_light); + HEARTRATE_LABEL = getContext().getString(R.string.charts_legend_heartrate); + akActivity = new ActivityConfig(ActivityKind.TYPE_ACTIVITY, getString(R.string.abstract_chart_fragment_kind_activity), AK_ACTIVITY_COLOR); akLightSleep = new ActivityConfig(ActivityKind.TYPE_LIGHT_SLEEP, getString(R.string.abstract_chart_fragment_kind_light_sleep), AK_LIGHT_SLEEP_COLOR); akDeepSleep = new ActivityConfig(ActivityKind.TYPE_DEEP_SLEEP, getString(R.string.abstract_chart_fragment_kind_deep_sleep), AK_DEEP_SLEEP_COLOR); @@ -153,7 +176,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { protected ChartsHost getChartsHost() { return (ChartsHost) getActivity(); } - + private void setEndDate(Date date) { getChartsHost().setEndDate(date); } @@ -315,6 +338,8 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } protected void configureChartDefaults(Chart chart) { + chart.setDescription(""); + // if enabled, the chart will always start at zero on the y-axis chart.setNoDataText(getString(R.string.chart_no_data_synchronize)); @@ -323,6 +348,8 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { // enable touch gestures chart.setTouchEnabled(true); + + setupLegend(chart); } protected void configureBarLineChartDefaults(BarLineChartBase chart) { @@ -349,7 +376,10 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { if (chartsHost.getDevice() != null) { mChartDirty = false; updateDateInfo(getStartDate(), getEndDate()); - createRefreshTask("Visualizing data", getActivity()).execute(); + if (refreshTask != null && refreshTask.getStatus() != AsyncTask.Status.FINISHED) { + refreshTask.cancel(true); + } + refreshTask = createRefreshTask("Visualizing data", getActivity()).execute(); } } } @@ -357,9 +387,9 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { /** * This method reads the data from the database, analyzes and prepares it for * the charts. This will be called from a background task, so there must not be - * any UI access. #renderCharts will be automatically called after this method. + * any UI access. #updateChartsInUIThread and #renderCharts will be automatically called after this method. */ - protected abstract void refreshInBackground(DBHandler db, GBDevice device); + protected abstract ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device); /** * Triggers the actual (re-) rendering of the chart. @@ -367,7 +397,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { */ protected abstract void renderCharts(); - protected void refresh(GBDevice gbDevice, BarLineChartBase chart, List samples) { + protected DefaultChartsData refresh(GBDevice gbDevice, List samples) { Calendar cal = GregorianCalendar.getInstance(); cal.clear(); Date date; @@ -375,11 +405,10 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { String dateStringTo = ""; LOG.info("" + getTitle() + ": number of samples:" + samples.size()); + CombinedData combinedData; if (samples.size() > 1) { - float movement_divisor; boolean annotate = true; boolean use_steps_as_movement; - SampleProvider provider = getProvider(gbDevice); int last_type = ActivityKind.TYPE_UNKNOWN; @@ -389,7 +418,10 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { int numEntries = samples.size(); List xLabels = new ArrayList<>(numEntries); List activityEntries = new ArrayList<>(numEntries); + boolean hr = supportsHeartrate(); + List heartrateEntries = hr ? new ArrayList(numEntries) : null; List colors = new ArrayList<>(numEntries); // this is kinda inefficient... + int lastHrSampleIndex = -1; for (int i = 0; i < numEntries; i++) { ActivitySample sample = samples.get(i); @@ -431,6 +463,15 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { colors.add(akActivity.color); } activityEntries.add(createBarEntry(value, i)); + if (hr && isValidHeartRateValue(sample.getCustomValue())) { + if (lastHrSampleIndex > -1 && i - lastHrSampleIndex > HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { + heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1)); + heartrateEntries.add(createLineEntry(0, i - 1)); + } + + heartrateEntries.add(createLineEntry(sample.getCustomValue(), i)); + lastHrSampleIndex = i; + } String xLabel = ""; if (annotate) { @@ -460,25 +501,34 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { xLabels.add(xLabel); } - chart.getXAxis().setValues(xLabels); +// chart.getXAxis().setValues(xLabels); BarDataSet activitySet = createActivitySet(activityEntries, colors, "Activity"); - - ArrayList dataSets = new ArrayList<>(); - dataSets.add(activitySet); - // create a data object with the datasets - BarData data = new BarData(xLabels, dataSets); - data.setGroupSpace(0); + combinedData = new CombinedData(xLabels); + List list = new ArrayList<>(); + list.add(activitySet); + BarData barData = new BarData(xLabels, list); + barData.setGroupSpace(0); + combinedData.setData(barData); + + if (hr && heartrateEntries.size() > 0) { + LineDataSet heartrateSet = createHeartrateSet(heartrateEntries, "Heart Rate"); + LineData lineData = new LineData(xLabels, heartrateSet); + combinedData.setData(lineData); + } - chart.setDescription(""); // chart.setDescription(getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo)); // chart.setDescriptionPosition(?, ?); - - setupLegend(chart); - - chart.setData(data); + } else { + combinedData = new CombinedData(Collections.emptyList()); } + + return new DefaultChartsData(combinedData); + } + + protected boolean isValidHeartRateValue(int value) { + return value > HeartRateUtils.MIN_HEART_RATE_VALUE && value < HeartRateUtils.MAX_HEART_RATE_VALUE; } /** @@ -498,6 +548,10 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { return new BarEntry(value, index); } + protected Entry createLineEntry(float value, int index) { + return new Entry(value, index); + } + protected BarDataSet createActivitySet(List values, List colors, String label) { BarDataSet set1 = new BarDataSet(values, label); set1.setColors(colors); @@ -512,6 +566,27 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { // set1.setHighLightColor(Color.rgb(128, 0, 255)); // set1.setColor(Color.rgb(89, 178, 44)); set1.setValueTextColor(CHART_TEXT_COLOR); + set1.setAxisDependency(YAxis.AxisDependency.LEFT); + return set1; + } + + protected LineDataSet createHeartrateSet(List values, String label) { + LineDataSet set1 = new LineDataSet(values, label); + set1.setLineWidth(0.8f); + set1.setColor(HEARTRATE_COLOR); + set1.setDrawCubic(true); + set1.setCubicIntensity(0.1f); + set1.setDrawCircles(false); +// set1.setCircleRadius(2f); +// set1.setDrawFilled(true); +// set1.setColor(getResources().getColor(android.R.color.background_light)); +// set1.setCircleColor(HEARTRATE_COLOR); +// set1.setFillColor(ColorTemplate.getHoloBlue()); +// set1.setHighLightColor(Color.rgb(128, 0, 255)); +// set1.setColor(Color.rgb(89, 178, 44)); + set1.setDrawValues(true); + set1.setValueTextColor(CHART_TEXT_COLOR); + set1.setAxisDependency(YAxis.AxisDependency.RIGHT); return set1; } @@ -554,6 +629,8 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } public class RefreshTask extends DBAccess { + private ChartsData chartsData; + public RefreshTask(String task, Context context) { super(task, context); } @@ -562,7 +639,9 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { protected void doInBackground(DBHandler db) { ChartsHost chartsHost = getChartsHost(); if (chartsHost != null) { - refreshInBackground(db, chartsHost.getDevice()); + chartsData = refreshInBackground(chartsHost, db, chartsHost.getDevice()); + } else { + cancel(true); } } @@ -571,6 +650,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { super.onPostExecute(o); FragmentActivity activity = getActivity(); if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) { + updateChartsnUIThread(chartsData); renderCharts(); } else { LOG.info("Not rendering charts because activity is not available anymore"); @@ -578,6 +658,8 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } } + protected abstract void updateChartsnUIThread(ChartsData chartsData); + /** * Returns true if the date was successfully shifted, and false if the shift * was ignored, e.g. when the to-value is in the future. @@ -621,4 +703,16 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { private int toTimestamp(Date date) { return (int) ((date.getTime() / 1000)); } + + public static class DefaultChartsData extends ChartsData { + private final CombinedData combinedData; + + public DefaultChartsData(CombinedData combinedData) { + this.combinedData = combinedData; + } + + public CombinedData getCombinedData() { + return combinedData; + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java index b60927b46..7260a205e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; @@ -70,6 +71,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment { // y.setDrawLabels(false); // TODO: make fixed max value optional y.setAxisMaxValue(1f); + y.setAxisMinValue(0); y.setDrawTopYLabelEntry(false); y.setTextColor(CHART_TEXT_COLOR); @@ -78,10 +80,12 @@ public class ActivitySleepChartFragment extends AbstractChartFragment { YAxis yAxisRight = mChart.getAxisRight(); yAxisRight.setDrawGridLines(false); - yAxisRight.setEnabled(false); - yAxisRight.setDrawLabels(false); - yAxisRight.setDrawTopYLabelEntry(false); + yAxisRight.setEnabled(supportsHeartrate()); + yAxisRight.setDrawLabels(true); + yAxisRight.setDrawTopYLabelEntry(true); yAxisRight.setTextColor(CHART_TEXT_COLOR); + yAxisRight.setAxisMaxValue(HeartRateUtils.MAX_HEART_RATE_VALUE); + yAxisRight.setAxisMinValue(HeartRateUtils.MIN_HEART_RATE_VALUE); // refresh immediately instead of use refreshIfVisible(), for perceived performance refresh(); @@ -103,11 +107,16 @@ public class ActivitySleepChartFragment extends AbstractChartFragment { } @Override - protected void refreshInBackground(DBHandler db, GBDevice device) { + protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { List samples = getSamples(db, device); - refresh(device, mChart, samples); + return refresh(device, samples); + } + @Override + protected void updateChartsnUIThread(ChartsData chartsData) { + DefaultChartsData dcd = (DefaultChartsData) chartsData; mChart.getLegend().setTextColor(LEGEND_TEXT_COLOR); + mChart.setData(dcd.getCombinedData()); } protected void renderCharts() { @@ -125,6 +134,10 @@ public class ActivitySleepChartFragment extends AbstractChartFragment { legendLabels.add(akDeepSleep.label); legendColors.add(akNotWorn.color); legendLabels.add(akNotWorn.label); + if (supportsHeartrate()) { + legendColors.add(HEARTRATE_COLOR); + legendLabels.add(HEARTRATE_LABEL); + } chart.getLegend().setCustom(legendColors, legendLabels); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsData.java new file mode 100644 index 000000000..4285951f8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsData.java @@ -0,0 +1,4 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +public abstract class ChartsData { +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CustomBarChart.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CustomBarChart.java index 788302781..131f130f9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CustomBarChart.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CustomBarChart.java @@ -49,6 +49,7 @@ public class CustomBarChart extends BarChart { /** * Call this to set the next value for the Entry to be animated. * Call animateY() when ready to do that. + * * @param nextValue */ public void setSingleEntryYValue(float nextValue) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java index 2e9d29b98..19a62a4b4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java @@ -23,6 +23,7 @@ import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.data.BarData; import com.github.mikephil.charting.data.BarDataSet; import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.ChartData; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineDataSet; @@ -39,10 +40,12 @@ import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +import nodomain.freeyourgadget.gadgetbridge.model.Measurement; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class LiveActivityFragment extends AbstractChartFragment { @@ -63,6 +66,9 @@ public class LiveActivityFragment extends AbstractChartFragment { private final Steps mSteps = new Steps(); private ScheduledExecutorService pulseScheduler; private int maxStepsResetCounter; + private List heartRateValues; + private LineDataSet mHeartRateSet; + private int mHeartRate; private class Steps { private int initialSteps; @@ -145,16 +151,36 @@ public class LiveActivityFragment extends AbstractChartFragment { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); switch (action) { - case DeviceService.ACTION_REALTIME_STEPS: + case DeviceService.ACTION_REALTIME_STEPS: { int steps = intent.getIntExtra(DeviceService.EXTRA_REALTIME_STEPS, 0); long timestamp = intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); - refreshCurrentSteps(steps, timestamp); + addEntries(steps, timestamp); break; + } + case DeviceService.ACTION_HEARTRATE_MEASUREMENT: { + int heartRate = intent.getIntExtra(DeviceService.EXTRA_HEART_RATE_VALUE, 0); + long timestamp = intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); + if (isValidHeartRateValue(heartRate)) { + setCurrentHeartRate(heartRate, timestamp); + } + break; + } } } }; - private void refreshCurrentSteps(int steps, long timestamp) { + private void setCurrentHeartRate(int heartRate, long timestamp) { + addHistoryDataSet(true); + mHeartRate = heartRate; + } + + private int getCurrentHeartRate() { + int result = mHeartRate; + mHeartRate = -1; + return result; + } + + private void addEntries(int steps, long timestamp) { mSteps.updateCurrentSteps(steps, timestamp); if (++maxStepsResetCounter > RESET_COUNT) { maxStepsResetCounter = 0; @@ -163,10 +189,10 @@ public class LiveActivityFragment extends AbstractChartFragment { // Or: count down the steps until goal reached? And then flash GOAL REACHED -> Set stretch goal LOG.info("Steps: " + steps + ", total: " + mSteps.getTotalSteps() + ", current: " + mSteps.getStepsPerMinute(false)); -// refreshCurrentSteps(); +// addEntries(); } - private void refreshCurrentSteps() { + private void addEntries() { mTotalStepsChart.setSingleEntryYValue(mSteps.getTotalSteps()); YAxis stepsPerMinuteCurrentYAxis = mStepsPerMinuteCurrentChart.getAxisLeft(); int maxStepsPerMinute = mSteps.getMaxStepsPerMinute(); @@ -180,24 +206,36 @@ public class LiveActivityFragment extends AbstractChartFragment { int stepsPerMinute = mSteps.getStepsPerMinute(true); mStepsPerMinuteCurrentChart.setSingleEntryYValue(stepsPerMinute); - if (mStepsPerMinuteHistoryChart.getData() == null) { - if (mSteps.getTotalSteps() == 0) { - return; // ignore the first default value to keep the "no-data-description" visible - } - LineData data = new LineData(); - mStepsPerMinuteHistoryChart.setData(data); - data.addDataSet(mHistorySet); + if (!addHistoryDataSet(false)) { + return; } - LineData historyData = (LineData) mStepsPerMinuteHistoryChart.getData(); - historyData.addXValue(""); - historyData.addEntry(new Entry(stepsPerMinute, mHistorySet.getEntryCount()), 0); + ChartData data = mStepsPerMinuteHistoryChart.getData(); + data.addXValue(""); + if (stepsPerMinute < 0) { + stepsPerMinute = 0; + } + mHistorySet.addEntry(new Entry(stepsPerMinute, data.getXValCount() - 1)); + int hr = getCurrentHeartRate(); + if (hr < 0) { + hr = 0; + } + mHeartRateSet.addEntry(new Entry(hr, data.getXValCount() - 1)); + } - mTotalStepsData.notifyDataSetChanged(); - mStepsPerMinuteData.notifyDataSetChanged(); - mStepsPerMinuteHistoryChart.notifyDataSetChanged(); - - renderCharts(); + private boolean addHistoryDataSet(boolean force) { + if (mStepsPerMinuteHistoryChart.getData() == null) { + // ignore the first default value to keep the "no-data-description" visible + if (force || mSteps.getTotalSteps() > 0) { + LineData data = new LineData(); + data.addDataSet(mHistorySet); + data.addDataSet(mHeartRateSet); + mStepsPerMinuteHistoryChart.setData(data); + return true; + } + return false; + } + return true; } @Nullable @@ -205,6 +243,8 @@ public class LiveActivityFragment extends AbstractChartFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { IntentFilter filterLocal = new IntentFilter(); filterLocal.addAction(DeviceService.ACTION_REALTIME_STEPS); + filterLocal.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT); + heartRateValues = new ArrayList<>(); View rootView = inflater.inflate(R.layout.fragment_live_activity, container, false); @@ -227,16 +267,12 @@ public class LiveActivityFragment extends AbstractChartFragment { @Override public void onPause() { super.onPause(); - if (pulseScheduler != null) { - pulseScheduler.shutdownNow(); - pulseScheduler = null; - } + stopActivityPulse(); } @Override public void onResume() { super.onResume(); - pulseScheduler = startActivityPulse(); } private ScheduledExecutorService startActivityPulse() { @@ -258,11 +294,33 @@ public class LiveActivityFragment extends AbstractChartFragment { return service; } + private void stopActivityPulse() { + if (pulseScheduler != null) { + pulseScheduler.shutdownNow(); + pulseScheduler = null; + } + } + /** * Called in the UI thread. */ private void pulse() { - refreshCurrentSteps(); + addEntries(); + + LineData historyData = (LineData) mStepsPerMinuteHistoryChart.getData(); + if (historyData == null) { + return; + } + + historyData.notifyDataChanged(); + mTotalStepsData.notifyDataSetChanged(); + mStepsPerMinuteData.notifyDataSetChanged(); + mStepsPerMinuteHistoryChart.notifyDataSetChanged(); + + renderCharts(); + + // have to enable it again and again to keep it measureing + GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true); } private long getPulseIntervalMillis() { @@ -272,15 +330,19 @@ public class LiveActivityFragment extends AbstractChartFragment { @Override protected void onMadeVisibleInActivity() { GBApplication.deviceService().onEnableRealtimeSteps(true); + GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true); super.onMadeVisibleInActivity(); if (getActivity() != null) { getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } + pulseScheduler = startActivityPulse(); } @Override protected void onMadeInvisibleInActivity() { + stopActivityPulse(); GBApplication.deviceService().onEnableRealtimeSteps(false); + GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(false); if (getActivity() != null) { getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @@ -289,6 +351,7 @@ public class LiveActivityFragment extends AbstractChartFragment { @Override public void onDestroyView() { + onMadeInvisibleInActivity(); LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mReceiver); super.onDestroyView(); } @@ -315,9 +378,9 @@ public class LiveActivityFragment extends AbstractChartFragment { List xLabels = new ArrayList<>(); List colors = new ArrayList<>(); - entries.add(new BarEntry(0,0)); + entries.add(new BarEntry(0, 0)); entries.add(entry); - entries.add(new BarEntry(0,2)); + entries.add(new BarEntry(0, 2)); colors.add(akActivity.color); colors.add(akActivity.color); colors.add(akActivity.color); @@ -346,6 +409,7 @@ public class LiveActivityFragment extends AbstractChartFragment { private void setupHistoryChart(BarLineChartBase chart) { configureBarLineChartDefaults(chart); + chart.setTouchEnabled(false); // no zooming or anything, because it's updated all the time chart.setBackgroundColor(BACKGROUND_COLOR); chart.setDescriptionColor(DESCRIPTION_COLOR); chart.setDescription(getString(R.string.live_activity_steps_per_minute_history)); @@ -367,22 +431,28 @@ public class LiveActivityFragment extends AbstractChartFragment { y.setDrawGridLines(false); y.setDrawTopYLabelEntry(false); y.setTextColor(CHART_TEXT_COLOR); - y.setEnabled(true); + y.setAxisMinValue(0); YAxis yAxisRight = chart.getAxisRight(); yAxisRight.setDrawGridLines(false); - yAxisRight.setEnabled(false); - yAxisRight.setDrawLabels(false); + yAxisRight.setEnabled(true); + yAxisRight.setDrawLabels(true); yAxisRight.setDrawTopYLabelEntry(false); yAxisRight.setTextColor(CHART_TEXT_COLOR); + yAxisRight.setAxisMaxValue(HeartRateUtils.MAX_HEART_RATE_VALUE); + yAxisRight.setAxisMinValue(HeartRateUtils.MIN_HEART_RATE_VALUE); mHistorySet = new LineDataSet(new ArrayList(), getString(R.string.live_activity_steps_history)); + mHistorySet.setAxisDependency(YAxis.AxisDependency.LEFT); mHistorySet.setColor(akActivity.color); mHistorySet.setDrawCircles(false); mHistorySet.setDrawCubic(true); mHistorySet.setDrawFilled(true); mHistorySet.setDrawValues(false); + + mHeartRateSet = createHeartrateSet(new ArrayList(), getString(R.string.live_activity_heart_rate)); + mHeartRateSet.setDrawValues(false); } @Override @@ -402,7 +472,13 @@ public class LiveActivityFragment extends AbstractChartFragment { } @Override - protected void refreshInBackground(DBHandler db, GBDevice device) { + protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { + throw new UnsupportedOperationException(); + } + + @Override + protected void updateChartsnUIThread(ChartsData chartsData) { + throw new UnsupportedOperationException(); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java index f2c2f37f1..48ff18f84 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java @@ -8,8 +8,8 @@ import android.view.View; import android.view.ViewGroup; import com.github.mikephil.charting.animation.Easing; -import com.github.mikephil.charting.charts.BarLineChartBase; import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.charts.CombinedChart; import com.github.mikephil.charting.charts.PieChart; import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.YAxis; @@ -27,6 +27,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount; @@ -39,7 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; public class SleepChartFragment extends AbstractChartFragment { protected static final Logger LOG = LoggerFactory.getLogger(ActivitySleepChartFragment.class); - private BarLineChartBase mActivityChart; + private CombinedChart mActivityChart; private PieChart mSleepAmountChart; private int mSmartAlarmFrom = -1; @@ -48,14 +49,16 @@ public class SleepChartFragment extends AbstractChartFragment { private int mSmartAlarmGoneOff = -1; @Override - protected void refreshInBackground(DBHandler db, GBDevice device) { + protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { List samples = getSamples(db, device); - refresh(device, mActivityChart, samples); - refreshSleepAmounts(device, mSleepAmountChart, samples); + MySleepChartsData mySleepChartsData = refreshSleepAmounts(device, samples); + DefaultChartsData chartsData = refresh(device, samples); + + return new MyChartsData(mySleepChartsData, chartsData); } - private void refreshSleepAmounts(GBDevice mGBDevice, PieChart pieChart, List samples) { + private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List samples) { ActivityAnalysis analysis = new ActivityAnalysis(); ActivityAmounts amounts = analysis.calculateActivityAmounts(samples); PieData data = new PieData(); @@ -73,7 +76,6 @@ public class SleepChartFragment extends AbstractChartFragment { } } String totalSleep = DateTimeUtils.formatDurationHoursMinutes(totalSeconds, TimeUnit.SECONDS); - pieChart.setCenterText(totalSleep); PieDataSet set = new PieDataSet(entries, ""); set.setValueFormatter(new ValueFormatter() { @Override @@ -83,10 +85,18 @@ public class SleepChartFragment extends AbstractChartFragment { }); set.setColors(colors); data.setDataSet(set); - pieChart.setData(data); - pieChart.getLegend().setEnabled(false); //setupLegend(pieChart); + return new MySleepChartsData(totalSleep, data); + } + + @Override + protected void updateChartsnUIThread(ChartsData chartsData) { + MyChartsData mcd = (MyChartsData) chartsData; + mSleepAmountChart.setCenterText(mcd.getPieData().getTotalSleep()); + mSleepAmountChart.setData(mcd.getPieData().getPieData()); + + mActivityChart.setData(mcd.getChartsData().getCombinedData()); } @Override @@ -99,7 +109,7 @@ public class SleepChartFragment extends AbstractChartFragment { Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_sleepchart, container, false); - mActivityChart = (BarLineChartBase) rootView.findViewById(R.id.sleepchart); + mActivityChart = (CombinedChart) rootView.findViewById(R.id.sleepchart); mSleepAmountChart = (PieChart) rootView.findViewById(R.id.sleepchart_pie_light_deep); setupActivityChart(); @@ -132,6 +142,7 @@ public class SleepChartFragment extends AbstractChartFragment { mSleepAmountChart.setDescription(""); mSleepAmountChart.setNoDataTextDescription(""); mSleepAmountChart.setNoDataText(""); + mSleepAmountChart.getLegend().setEnabled(false); } private void setupActivityChart() { @@ -151,6 +162,7 @@ public class SleepChartFragment extends AbstractChartFragment { // y.setDrawLabels(false); // TODO: make fixed max value optional y.setAxisMaxValue(1f); + y.setAxisMinValue(0); y.setDrawTopYLabelEntry(false); y.setTextColor(CHART_TEXT_COLOR); @@ -159,10 +171,12 @@ public class SleepChartFragment extends AbstractChartFragment { YAxis yAxisRight = mActivityChart.getAxisRight(); yAxisRight.setDrawGridLines(false); - yAxisRight.setEnabled(false); - yAxisRight.setDrawLabels(false); - yAxisRight.setDrawTopYLabelEntry(false); + yAxisRight.setEnabled(supportsHeartrate()); + yAxisRight.setDrawLabels(true); + yAxisRight.setDrawTopYLabelEntry(true); yAxisRight.setTextColor(CHART_TEXT_COLOR); + yAxisRight.setAxisMaxValue(HeartRateUtils.MAX_HEART_RATE_VALUE); + yAxisRight.setAxisMinValue(HeartRateUtils.MIN_HEART_RATE_VALUE); } protected void setupLegend(Chart chart) { @@ -172,6 +186,10 @@ public class SleepChartFragment extends AbstractChartFragment { legendLabels.add(akLightSleep.label); legendColors.add(akDeepSleep.color); legendLabels.add(akDeepSleep.label); + if (supportsHeartrate()) { + legendColors.add(HEARTRATE_COLOR); + legendLabels.add(HEARTRATE_LABEL); + } chart.getLegend().setCustom(legendColors, legendLabels); chart.getLegend().setTextColor(LEGEND_TEXT_COLOR); } @@ -187,4 +205,40 @@ public class SleepChartFragment extends AbstractChartFragment { mActivityChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart); mSleepAmountChart.invalidate(); } + + private static class MySleepChartsData extends ChartsData { + private String totalSleep; + private final PieData pieData; + + public MySleepChartsData(String totalSleep, PieData pieData) { + this.totalSleep = totalSleep; + this.pieData = pieData; + } + + public PieData getPieData() { + return pieData; + } + + public CharSequence getTotalSleep() { + return totalSleep; + } + } + + private static class MyChartsData extends ChartsData { + private final DefaultChartsData chartsData; + private final MySleepChartsData pieData; + + public MyChartsData(MySleepChartsData pieData, DefaultChartsData chartsData) { + this.pieData = pieData; + this.chartsData = chartsData; + } + + public MySleepChartsData getPieData() { + return pieData; + } + + public DefaultChartsData getChartsData() { + return chartsData; + } + } } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java index 713ef030e..2e271f286 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java @@ -6,8 +6,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import com.github.mikephil.charting.charts.BarLineChartBase; import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.charts.CombinedChart; import com.github.mikephil.charting.charts.PieChart; import com.github.mikephil.charting.components.LimitLine; import com.github.mikephil.charting.components.XAxis; @@ -15,6 +15,7 @@ import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.data.BarData; import com.github.mikephil.charting.data.BarDataSet; import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.data.CombinedData; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.PieData; import com.github.mikephil.charting.data.PieDataSet; @@ -41,19 +42,30 @@ public class WeekStepsChartFragment extends AbstractChartFragment { private Locale mLocale; private int mTargetSteps = 10000; - private BarLineChartBase mWeekStepsChart; private PieChart mTodayStepsChart; + private CombinedChart mWeekStepsChart; @Override - protected void refreshInBackground(DBHandler db, GBDevice device) { - ChartsHost chartsHost = getChartsHost(); - if (chartsHost != null) { - Calendar day = Calendar.getInstance(); - day.setTime(chartsHost.getEndDate()); - //NB: we could have omitted the day, but this way we can move things to the past easily - refreshDaySteps(db, mTodayStepsChart, day, device); - refreshWeekBeforeSteps(db, mWeekStepsChart, day, device); - } + protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { + Calendar day = Calendar.getInstance(); + day.setTime(chartsHost.getEndDate()); + //NB: we could have omitted the day, but this way we can move things to the past easily + DaySteps daySteps = refreshDaySteps(db, day, device); + DefaultChartsData weekBeforeStepsData = refreshWeekBeforeSteps(db, mWeekStepsChart, day, device); + + return new MyChartsData(daySteps, weekBeforeStepsData); + } + + @Override + protected void updateChartsnUIThread(ChartsData chartsData) { + MyChartsData mcd = (MyChartsData) chartsData; + +// setupLegend(mWeekStepsChart); + mTodayStepsChart.setCenterText(NumberFormat.getNumberInstance(mLocale).format(mcd.getDaySteps().totalSteps)); + mTodayStepsChart.setData(mcd.getDaySteps().data); + + mWeekStepsChart.setData(mcd.getWeekBeforeStepsData().getCombinedData()); + mWeekStepsChart.getLegend().setEnabled(false); } @Override @@ -62,7 +74,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment { mTodayStepsChart.invalidate(); } - private void refreshWeekBeforeSteps(DBHandler db, BarLineChartBase barChart, Calendar day, GBDevice device) { + private DefaultChartsData refreshWeekBeforeSteps(DBHandler db, CombinedChart combinedChart, Calendar day, GBDevice device) { ActivityAnalysis analysis = new ActivityAnalysis(); @@ -80,24 +92,25 @@ public class WeekStepsChartFragment extends AbstractChartFragment { BarDataSet set = new BarDataSet(entries, ""); set.setColor(akActivity.color); - BarData data = new BarData(labels, set); - data.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false); + BarData barData = new BarData(labels, set); + barData.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false); LimitLine target = new LimitLine(mTargetSteps); - barChart.getAxisLeft().removeAllLimitLines(); - barChart.getAxisLeft().addLimitLine(target); + combinedChart.getAxisLeft().removeAllLimitLines(); + combinedChart.getAxisLeft().addLimitLine(target); - setupLegend(barChart); - barChart.setData(data); - barChart.getLegend().setEnabled(false); + CombinedData combinedData = new CombinedData(labels); + combinedData.setData(barData); + return new DefaultChartsData(combinedData); } - private void refreshDaySteps(DBHandler db, PieChart pieChart, Calendar day, GBDevice device) { + + + private DaySteps refreshDaySteps(DBHandler db, Calendar day, GBDevice device) { ActivityAnalysis analysis = new ActivityAnalysis(); int totalSteps = analysis.calculateTotalSteps(getSamplesOfDay(db, day, device)); - pieChart.setCenterText(NumberFormat.getNumberInstance(mLocale).format(totalSteps)); PieData data = new PieData(); List entries = new ArrayList<>(); List colors = new ArrayList<>(); @@ -119,9 +132,8 @@ public class WeekStepsChartFragment extends AbstractChartFragment { data.setDataSet(set); //this hides the values (numeric) added to the set. These would be shown aside the strings set with addXValue above data.setDrawValues(false); - pieChart.setData(data); - pieChart.getLegend().setEnabled(false); + return new DaySteps(data, totalSteps); } @Override @@ -137,7 +149,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment { mTargetSteps = MiBandCoordinator.getFitnessGoal(device.getAddress()); } - mWeekStepsChart = (BarLineChartBase) rootView.findViewById(R.id.sleepchart); + mWeekStepsChart = (CombinedChart) rootView.findViewById(R.id.sleepchart); mTodayStepsChart = (PieChart) rootView.findViewById(R.id.sleepchart_pie_light_deep); setupWeekStepsChart(); @@ -160,6 +172,8 @@ public class WeekStepsChartFragment extends AbstractChartFragment { mTodayStepsChart.setDescription(getContext().getString(R.string.weeksteps_today_steps_description, mTargetSteps)); mTodayStepsChart.setNoDataTextDescription(""); mTodayStepsChart.setNoDataText(""); + mTodayStepsChart.getLegend().setEnabled(false); +// setupLegend(mTodayStepsChart); } private void setupWeekStepsChart() { @@ -192,12 +206,12 @@ public class WeekStepsChartFragment extends AbstractChartFragment { } protected void setupLegend(Chart chart) { - List legendColors = new ArrayList<>(1); - List legendLabels = new ArrayList<>(1); - legendColors.add(akActivity.color); - legendLabels.add(getContext().getString(R.string.chart_steps)); - chart.getLegend().setCustom(legendColors, legendLabels); - chart.getLegend().setTextColor(LEGEND_TEXT_COLOR); +// List legendColors = new ArrayList<>(1); +// List legendLabels = new ArrayList<>(1); +// legendColors.add(akActivity.color); +// legendLabels.add(getContext().getString(R.string.chart_steps)); +// chart.getLegend().setCustom(legendColors, legendLabels); +// chart.getLegend().setTextColor(LEGEND_TEXT_COLOR); } private List getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) { @@ -222,4 +236,32 @@ public class WeekStepsChartFragment extends AbstractChartFragment { protected List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { return super.getAllSamples(db, device, tsFrom, tsTo); } + + private static class DaySteps { + private final PieData data; + private final int totalSteps; + + public DaySteps(PieData data, int totalSteps) { + this.data = data; + this.totalSteps = totalSteps; + } + } + + private static class MyChartsData extends ChartsData { + private final DefaultChartsData weekBeforeStepsData; + private final DaySteps daySteps; + + public MyChartsData(DaySteps daySteps, DefaultChartsData weekBeforeStepsData) { + this.daySteps = daySteps; + this.weekBeforeStepsData = weekBeforeStepsData; + } + + public DaySteps getDaySteps() { + return daySteps; + } + + public DefaultChartsData getWeekBeforeStepsData() { + return weekBeforeStepsData; + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBAlarmListAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBAlarmListAdapter.java index fec4c9290..5a5b7b3e5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBAlarmListAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBAlarmListAdapter.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Set; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms; import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm; @@ -152,7 +153,7 @@ public class GBAlarmListAdapter extends ArrayAdapter { if (isOn) { view.setTextColor(Color.BLUE); } else { - view.setTextColor(Color.BLACK); + view.setTextColor(GBApplication.getTextColor(mContext)); } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java index d4b555c38..92ca09b5f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java @@ -8,14 +8,17 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; +import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; +import java.util.Collections; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails; /** * Adapter for displaying GBDevice instances. @@ -32,7 +35,7 @@ public class GBDeviceAdapter extends ArrayAdapter { @Override public View getView(int position, View view, ViewGroup parent) { - GBDevice device = getItem(position); + final GBDevice device = getItem(position); if (view == null) { LayoutInflater inflater = (LayoutInflater) context @@ -42,33 +45,60 @@ public class GBDeviceAdapter extends ArrayAdapter { } TextView deviceStatusLabel = (TextView) view.findViewById(R.id.device_status); TextView deviceNameLabel = (TextView) view.findViewById(R.id.device_name); - TextView deviceInfoLabel = (TextView) view.findViewById(R.id.device_info); + final ListView deviceInfoList = (ListView) view.findViewById(R.id.device_item_infos); + ItemWithDetailsAdapter infoAdapter = new ItemWithDetailsAdapter(context, device.getDeviceInfos()); + infoAdapter.setHorizontalAlignment(true); + deviceInfoList.setAdapter(infoAdapter); + TextView batteryLabel = (TextView) view.findViewById(R.id.battery_label); TextView batteryStatusLabel = (TextView) view.findViewById(R.id.battery_status); - ImageView deviceImageView = (ImageView) view.findViewById(R.id.device_image); + final ImageView deviceImageView = (ImageView) view.findViewById(R.id.device_image); + ImageView deviceInfoView = (ImageView) view.findViewById(R.id.device_info_image); ProgressBar busyIndicator = (ProgressBar) view.findViewById(R.id.device_busy_indicator); deviceNameLabel.setText(getUniqueDeviceName(device)); - deviceInfoLabel.setText(device.getInfoString()); if (device.isBusy()) { deviceStatusLabel.setText(device.getBusyTask()); busyIndicator.setVisibility(View.VISIBLE); - batteryStatusLabel.setVisibility(View.GONE); - deviceInfoLabel.setVisibility(View.GONE); + batteryLabel.setVisibility(View.INVISIBLE); + batteryStatusLabel.setVisibility(View.INVISIBLE); } else { deviceStatusLabel.setText(device.getStateString()); - busyIndicator.setVisibility(View.GONE); + busyIndicator.setVisibility(View.INVISIBLE); + batteryLabel.setVisibility(View.VISIBLE); batteryStatusLabel.setVisibility(View.VISIBLE); - deviceInfoLabel.setVisibility(View.VISIBLE); } + boolean showInfoIcon = device.hasDeviceInfos() && !device.isBusy(); + deviceInfoView.setVisibility(showInfoIcon ? View.VISIBLE : View.GONE); + deviceInfoView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (deviceInfoList.getVisibility() == View.VISIBLE) { + deviceInfoList.setVisibility(View.GONE); + } else { + ArrayAdapter adapter = (ArrayAdapter) deviceInfoList.getAdapter(); + adapter.clear(); + List infos = device.getDeviceInfos(); + Collections.sort(infos); + adapter.addAll(infos); + justifyListViewHeightBasedOnChildren(deviceInfoList); + deviceInfoList.setVisibility(View.VISIBLE); + deviceInfoList.setFocusable(false); + } + } + }); + short batteryLevel = device.getBatteryLevel(); if (batteryLevel != GBDevice.BATTERY_UNKNOWN) { - batteryStatusLabel.setText("BAT: " + device.getBatteryLevel() + "%"); + batteryLabel.setText("BAT:"); + batteryStatusLabel.setText(device.getBatteryLevel() + "%"); BatteryState batteryState = device.getBatteryState(); if (BatteryState.BATTERY_LOW.equals(batteryState)) { + batteryLabel.setTextColor(Color.RED); batteryStatusLabel.setTextColor(Color.RED); } else { + batteryLabel.setTextColor(ContextCompat.getColor(getContext(), R.color.secondarytext)); batteryStatusLabel.setTextColor(ContextCompat.getColor(getContext(), R.color.secondarytext)); if (BatteryState.BATTERY_CHARGING.equals(batteryState) || @@ -77,27 +107,67 @@ public class GBDeviceAdapter extends ArrayAdapter { } } } else { + batteryLabel.setText(""); batteryStatusLabel.setText(""); } switch (device.getType()) { case PEBBLE: - deviceImageView.setImageResource(R.drawable.ic_device_pebble); + if (device.isConnected()) { + deviceImageView.setImageResource(R.drawable.ic_device_pebble); + } else { + deviceImageView.setImageResource(R.drawable.ic_device_pebble_disabled); + } break; case MIBAND: - deviceImageView.setImageResource(R.drawable.ic_device_miband); + if (device.isConnected()) { + deviceImageView.setImageResource(R.drawable.ic_device_miband); + } else { + deviceImageView.setImageResource(R.drawable.ic_device_miband_disabled); + } break; default: - deviceImageView.setImageResource(R.drawable.ic_launcher); + if (device.isConnected()) { + deviceImageView.setImageResource(R.drawable.ic_launcher); + } else { + deviceImageView.setImageResource(R.drawable.ic_device_default_disabled); + } } return view; } + public void justifyListViewHeightBasedOnChildren(ListView listView) { + ArrayAdapter adapter = (ArrayAdapter) listView.getAdapter(); + + if (adapter == null) { + return; + } + ViewGroup vg = listView; + int totalHeight = 0; + for (int i = 0; i < adapter.getCount(); i++) { + View listItem = adapter.getView(i, null, vg); + listItem.measure(0, 0); + totalHeight += listItem.getMeasuredHeight(); + } + + ViewGroup.LayoutParams par = listView.getLayoutParams(); + par.height = totalHeight + (listView.getDividerHeight() * (adapter.getCount() - 1)); + listView.setLayoutParams(par); + listView.requestLayout(); + } + private String getUniqueDeviceName(GBDevice device) { String deviceName = device.getName(); if (!isUniqueDeviceName(device, deviceName)) { - deviceName = deviceName + " " + device.getShortAddress(); + if (device.getHardwareVersion() != null) { + deviceName = deviceName + " " + device.getHardwareVersion(); + if (!isUniqueDeviceName(device, deviceName)) { + deviceName = deviceName + " " + device.getShortAddress(); + } + } else { + deviceName = deviceName + " " + device.getShortAddress(); + } } return deviceName; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java index dd1ab2da9..dcfee994f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java @@ -18,7 +18,12 @@ import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails; */ public class ItemWithDetailsAdapter extends ArrayAdapter { + public static final int SIZE_SMALL = 1; + public static final int SIZE_MEDIUM = 2; + public static final int SIZE_LARGE = 3; private final Context context; + private boolean horizontalAlignment; + private int size = SIZE_MEDIUM; public ItemWithDetailsAdapter(Context context, List items) { super(context, 0, items); @@ -26,6 +31,10 @@ public class ItemWithDetailsAdapter extends ArrayAdapter { this.context = context; } + public void setHorizontalAlignment(boolean horizontalAlignment) { + this.horizontalAlignment = horizontalAlignment; + } + @Override public View getView(int position, View view, ViewGroup parent) { ItemWithDetails item = getItem(position); @@ -34,7 +43,18 @@ public class ItemWithDetailsAdapter extends ArrayAdapter { LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = inflater.inflate(R.layout.item_with_details, parent, false); + if (horizontalAlignment) { + view = inflater.inflate(R.layout.item_with_details_horizontal, parent, false); + } else { + switch (size) { + case SIZE_SMALL: + view = inflater.inflate(R.layout.item_with_details_small, parent, false); + break; + default: + view = inflater.inflate(R.layout.item_with_details, parent, false); + break; + } + } } ImageView iconView = (ImageView) view.findViewById(R.id.item_image); TextView nameView = (TextView) view.findViewById(R.id.item_name); @@ -46,4 +66,12 @@ public class ItemWithDetailsAdapter extends ArrayAdapter { return view; } + + public void setSize(int size) { + this.size = size; + } + + public int getSize() { + return size; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/contentprovider/PebbleContentProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/contentprovider/PebbleContentProvider.java index c20a552dd..d02355211 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/contentprovider/PebbleContentProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/contentprovider/PebbleContentProvider.java @@ -6,16 +6,16 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; -import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.content.LocalBroadcastManager; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class PebbleContentProvider extends ContentProvider { @@ -59,8 +59,8 @@ public class PebbleContentProvider extends ContentProvider { MatrixCursor mc = new MatrixCursor(columnNames); int connected = 0; int appMessage = 0; - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this.getContext()); - if (sharedPrefs.getBoolean("pebble_enable_pebblekit", false)) { + Prefs prefs = GBApplication.getPrefs(); + if (prefs.getBoolean("pebble_enable_pebblekit", false)) { appMessage = 1; } if (mGBDevice != null && mGBDevice.getType() == DeviceType.PEBBLE && mGBDevice.isInitialized()) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java index d0263b572..ce9e1ae46 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java @@ -22,6 +22,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.util.GB; import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.DATABASE_NAME; +import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_CUSTOM_SHORT; import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_INTENSITY; import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER; import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS; @@ -33,7 +34,7 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl private static final Logger LOG = LoggerFactory.getLogger(ActivityDatabaseHandler.class); - private static final int DATABASE_VERSION = 5; + private static final int DATABASE_VERSION = 7; public ActivityDatabaseHandler(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -51,6 +52,7 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + LOG.info("ActivityDatabase: schema upgrade requested from " + oldVersion + " to " + newVersion); try { for (int i = oldVersion + 1; i <= newVersion; i++) { DBUpdateScript updater = getUpdateScript(db, i); @@ -68,6 +70,7 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + LOG.info("ActivityDatabase: schema downgrade requested from " + oldVersion + " to " + newVersion); try { for (int i = oldVersion; i >= newVersion; i--) { DBUpdateScript updater = getUpdateScript(db, i); @@ -101,6 +104,7 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl values.put(KEY_PROVIDER, sample.getProvider().getID()); values.put(KEY_INTENSITY, sample.getRawIntensity()); values.put(KEY_STEPS, sample.getSteps()); + values.put(KEY_CUSTOM_SHORT, sample.getCustomValue()); values.put(KEY_TYPE, sample.getRawKind()); db.insert(TABLE_GBACTIVITYSAMPLES, null, values); @@ -110,14 +114,15 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl /** * Adds the a new sample to the database * - * @param timestamp the timestamp of the same, second-based! - * @param provider the SampleProvider ID - * @param intensity the sample's raw intensity value - * @param steps the sample's steps value - * @param kind the raw activity kind of the sample + * @param timestamp the timestamp of the same, second-based! + * @param provider the SampleProvider ID + * @param intensity the sample's raw intensity value + * @param steps the sample's steps value + * @param kind the raw activity kind of the sample + * @param customShortValue */ @Override - public void addGBActivitySample(int timestamp, byte provider, short intensity, short steps, byte kind) { + public void addGBActivitySample(int timestamp, int provider, int intensity, int steps, int kind, int customShortValue) { if (intensity < 0) { LOG.error("negative intensity received, ignoring"); intensity = 0; @@ -127,6 +132,11 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl steps = 0; } + if (customShortValue < 0) { + LOG.error("negative short value received, ignoring"); + customShortValue = 0; + } + try (SQLiteDatabase db = this.getWritableDatabase()) { ContentValues values = new ContentValues(); values.put(KEY_TIMESTAMP, timestamp); @@ -134,6 +144,7 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl values.put(KEY_INTENSITY, intensity); values.put(KEY_STEPS, steps); values.put(KEY_TYPE, kind); + values.put(KEY_CUSTOM_SHORT, customShortValue); db.insert(TABLE_GBACTIVITYSAMPLES, null, values); } @@ -144,8 +155,8 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl try (SQLiteDatabase db = this.getWritableDatabase()) { String sql = "INSERT INTO " + TABLE_GBACTIVITYSAMPLES + " (" + KEY_TIMESTAMP + "," + - KEY_PROVIDER + "," + KEY_INTENSITY + "," + KEY_STEPS + "," + KEY_TYPE + ")" + - " VALUES (?,?,?,?,?);"; + KEY_PROVIDER + "," + KEY_INTENSITY + "," + KEY_STEPS + "," + KEY_TYPE + "," + KEY_CUSTOM_SHORT + ")" + + " VALUES (?,?,?,?,?,?);"; SQLiteStatement statement = db.compileStatement(sql); db.beginTransaction(); @@ -156,6 +167,7 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl statement.bindLong(3, activitySample.getRawIntensity()); statement.bindLong(4, activitySample.getSteps()); statement.bindLong(5, activitySample.getRawKind()); + statement.bindLong(6, activitySample.getCustomValue()); statement.execute(); } db.setTransactionSuccessful(); @@ -209,16 +221,20 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl try (SQLiteDatabase db = this.getReadableDatabase()) { try (Cursor cursor = db.query(TABLE_GBACTIVITYSAMPLES, null, where, null, null, null, order)) { LOG.info("Activity query result: " + cursor.getCount() + " samples"); - if (cursor.moveToFirst()) { - do { - GBActivitySample sample = new GBActivitySample( - provider, - cursor.getInt(cursor.getColumnIndex(KEY_TIMESTAMP)), - cursor.getShort(cursor.getColumnIndex(KEY_INTENSITY)), - cursor.getShort(cursor.getColumnIndex(KEY_STEPS)), - (byte) cursor.getShort(cursor.getColumnIndex(KEY_TYPE))); - samples.add(sample); - } while (cursor.moveToNext()); + int colTimeStamp = cursor.getColumnIndex(KEY_TIMESTAMP); + int colIntensity = cursor.getColumnIndex(KEY_INTENSITY); + int colSteps = cursor.getColumnIndex(KEY_STEPS); + int colType = cursor.getColumnIndex(KEY_TYPE); + int colCustomShort = cursor.getColumnIndex(KEY_CUSTOM_SHORT); + while (cursor.moveToNext()) { + GBActivitySample sample = new GBActivitySample( + provider, + cursor.getInt(colTimeStamp), + cursor.getInt(colIntensity), + cursor.getInt(colSteps), + cursor.getInt(colType), + cursor.getInt(colCustomShort)); + samples.add(sample); } } } @@ -232,7 +248,7 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl } StringBuilder builder = new StringBuilder(" and ("); - byte[] dbActivityTypes = ActivityKind.mapToDBActivityTypes(activityTypes, provider); + int[] dbActivityTypes = ActivityKind.mapToDBActivityTypes(activityTypes, provider); for (int i = 0; i < dbActivityTypes.length; i++) { builder.append(" type=").append(dbActivityTypes[i]); if (i + 1 < dbActivityTypes.length) { @@ -242,4 +258,50 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl builder.append(')'); return builder.toString(); } + + @Override + public void changeStoredSamplesType(int timestampFrom, int timestampTo, int kind, SampleProvider provider) { + try (SQLiteDatabase db = this.getReadableDatabase()) { + String sql = "UPDATE " + TABLE_GBACTIVITYSAMPLES + " SET " + KEY_TYPE + "= ? WHERE " + + KEY_PROVIDER + " = ? AND " + + KEY_TIMESTAMP + " >= ? AND " + KEY_TIMESTAMP + " < ? ;"; //do not use BETWEEN because the range is inclusive in that case! + + SQLiteStatement statement = db.compileStatement(sql); + statement.bindLong(1, kind); + statement.bindLong(2, provider.getID()); + statement.bindLong(3, timestampFrom); + statement.bindLong(4, timestampTo); + statement.execute(); + } + } + + @Override + public void changeStoredSamplesType(int timestampFrom, int timestampTo, int fromKind, int toKind, SampleProvider provider) { + try (SQLiteDatabase db = this.getReadableDatabase()) { + String sql = "UPDATE " + TABLE_GBACTIVITYSAMPLES + " SET " + KEY_TYPE + "= ? WHERE " + + KEY_TYPE + " = ? AND " + + KEY_PROVIDER + " = ? AND " + + KEY_TIMESTAMP + " >= ? AND " + KEY_TIMESTAMP + " < ? ;"; //do not use BETWEEN because the range is inclusive in that case! + + SQLiteStatement statement = db.compileStatement(sql); + statement.bindLong(1, toKind); + statement.bindLong(2, fromKind); + statement.bindLong(3, provider.getID()); + statement.bindLong(4, timestampFrom); + statement.bindLong(5, timestampTo); + statement.execute(); + } + } + + @Override + public int fetchLatestTimestamp(SampleProvider provider) { + try (SQLiteDatabase db = this.getReadableDatabase()) { + try (Cursor cursor = db.query(TABLE_GBACTIVITYSAMPLES, new String[]{KEY_TIMESTAMP}, KEY_PROVIDER + "=" + String.valueOf(provider.getID()), null, null, null, KEY_TIMESTAMP + " DESC", "1")) { + if (cursor.moveToFirst()) { + return cursor.getInt(0); + } + } + } + return -1; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java index 696f30d72..5d1a72b4d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java @@ -10,5 +10,6 @@ public class DBConstants { public static final String KEY_PROVIDER = "provider"; public static final String KEY_INTENSITY = "intensity"; public static final String KEY_STEPS = "steps"; + public static final String KEY_CUSTOM_SHORT = "customShort"; public static final String KEY_TYPE = "type"; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java index 54e33829d..49dbbd400 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java @@ -24,9 +24,16 @@ public interface DBHandler { List getSleepSamples(int tsFrom, int tsTo, SampleProvider provider); - void addGBActivitySample(int timestamp, byte provider, short intensity, short steps, byte kind); + void addGBActivitySample(int timestamp, int provider, int intensity, int steps, int kind, int heartrate); void addGBActivitySamples(ActivitySample[] activitySamples); SQLiteDatabase getWritableDatabase(); + + void changeStoredSamplesType(int timestampFrom, int timestampTo, int kind, SampleProvider provider); + + void changeStoredSamplesType(int timestampFrom, int timestampTo, int fromKind, int toKind, SampleProvider provider); + + int fetchLatestTimestamp(SampleProvider provider); + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java index a5fed0102..7c53d3265 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java @@ -1,6 +1,7 @@ package nodomain.freeyourgadget.gadgetbridge.database; import android.content.Context; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; @@ -70,6 +71,22 @@ public class DBHelper { db.execSQL(statement); } + public static boolean existsColumn(String tableName, String columnName, SQLiteDatabase db) { + try (Cursor res = db.rawQuery("PRAGMA table_info('" + tableName + "')", null)) { + int index = res.getColumnIndex("name"); + if (index < 1) { + return false; // something's really wrong + } + while (res.moveToNext()) { + String cn = res.getString(index); + if (columnName.equals(cn)) { + return true; + } + } + } + return false; + } + /** * WITHOUT ROWID is only available with sqlite 3.8.2, which is available * with Lollipop and later. diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBCreationScript.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBCreationScript.java index 9aa67c862..20c4ac5b7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBCreationScript.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBCreationScript.java @@ -4,6 +4,7 @@ import android.database.sqlite.SQLiteDatabase; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_CUSTOM_SHORT; import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_INTENSITY; import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER; import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS; @@ -19,6 +20,7 @@ public class ActivityDBCreationScript { + KEY_INTENSITY + " SMALLINT," + KEY_STEPS + " TINYINT," + KEY_TYPE + " TINYINT," + + KEY_CUSTOM_SHORT + " INT," + " PRIMARY KEY (" + KEY_TIMESTAMP + "," + KEY_PROVIDER + ") ON CONFLICT REPLACE)" + DBHelper.getWithoutRowId(); db.execSQL(CREATE_GBACTIVITYSAMPLES_TABLE); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_6.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_6.java index f23033670..b6d157c9c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_6.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_6.java @@ -5,27 +5,23 @@ import android.database.sqlite.SQLiteDatabase; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript; -import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER; -import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS; -import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP; -import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_STEPS_PER_DAY; +import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_CUSTOM_SHORT; +import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES; /** - * Adds a table "STEPS_PER_DAY". + * Adds a column "customShort" to the table "GBActivitySamples" */ public class ActivityDBUpdate_6 implements DBUpdateScript { @Override public void upgradeSchema(SQLiteDatabase db) { - String CREATE_STEPS_PER_DAY_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_STEPS_PER_DAY + " (" - + KEY_TIMESTAMP + " INT," - + KEY_PROVIDER + " TINYINT," - + KEY_STEPS + " MEDIUMINT," - + " PRIMARY KEY (" + KEY_TIMESTAMP + "," + KEY_PROVIDER + ") ON CONFLICT REPLACE)" + DBHelper.getWithoutRowId(); - db.execSQL(CREATE_STEPS_PER_DAY_TABLE); + if (!DBHelper.existsColumn(TABLE_GBACTIVITYSAMPLES, KEY_CUSTOM_SHORT, db)) { + String ADD_COLUMN_CUSTOM_SHORT = "ALTER TABLE " + TABLE_GBACTIVITYSAMPLES + " ADD COLUMN " + + KEY_CUSTOM_SHORT + " INT;"; + db.execSQL(ADD_COLUMN_CUSTOM_SHORT); + } } @Override public void downgradeSchema(SQLiteDatabase db) { - DBHelper.dropTable(TABLE_STEPS_PER_DAY, db); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_7.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_7.java new file mode 100644 index 000000000..c7077451f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_7.java @@ -0,0 +1,8 @@ +package nodomain.freeyourgadget.gadgetbridge.database.schema; + +/** + * Bugfix for users who installed 0.8.1 cleanly, i.e. without any previous + * database. Perform Update script 6 again. + */ +public class ActivityDBUpdate_7 extends ActivityDBUpdate_6 { +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_X.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_X.java new file mode 100644 index 000000000..e23d13e50 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_X.java @@ -0,0 +1,31 @@ +package nodomain.freeyourgadget.gadgetbridge.database.schema; + +import android.database.sqlite.SQLiteDatabase; + +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript; + +import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER; +import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS; +import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP; +import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_STEPS_PER_DAY; + +/** + * Adds a table "STEPS_PER_DAY". + */ +public class ActivityDBUpdate_X implements DBUpdateScript { + @Override + public void upgradeSchema(SQLiteDatabase db) { + String CREATE_STEPS_PER_DAY_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_STEPS_PER_DAY + " (" + + KEY_TIMESTAMP + " INT," + + KEY_PROVIDER + " TINYINT," + + KEY_STEPS + " MEDIUMINT," + + " PRIMARY KEY (" + KEY_TIMESTAMP + "," + KEY_PROVIDER + ") ON CONFLICT REPLACE)" + DBHelper.getWithoutRowId(); + db.execSQL(CREATE_STEPS_PER_DAY_TABLE); + } + + @Override + public void downgradeSchema(SQLiteDatabase db) { + DBHelper.dropTable(TABLE_STEPS_PER_DAY, db); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventDisplayMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventDisplayMessage.java new file mode 100644 index 000000000..29e5ed12f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventDisplayMessage.java @@ -0,0 +1,22 @@ +package nodomain.freeyourgadget.gadgetbridge.deviceevents; + +public class GBDeviceEventDisplayMessage { + public String message; + public int duration; + public int severity; + + /** + * An event for displaying a message to the user. How the message is displayed + * is a detail of the current activity, which needs to listen to the Intent + * GB.ACTION_DISPLAY_MESSAGE. + * + * @param message + * @param duration + * @param severity + */ + public GBDeviceEventDisplayMessage(String message, int duration, int severity) { + this.message = message; + this.duration = duration; + this.severity = severity; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index 8619875d3..7ecc3e8d1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -12,7 +12,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; * This interface is implemented at least once for every supported gadget device. * It allows Gadgetbridge to generically deal with different kinds of devices * without actually knowing the details of any device. - * + *

* Instances will be created as needed and asked whether they support a given * device. If a coordinator answers true, it will be used to assist in handling * the given device. @@ -22,6 +22,7 @@ public interface DeviceCoordinator { /** * Checks whether this candidate handles the given candidate. + * * @param candidate * @return true if this coordinator handles the given candidate. */ @@ -29,6 +30,7 @@ public interface DeviceCoordinator { /** * Checks whether this candidate handles the given device. + * * @param device * @return true if this coordinator handles the given device. */ @@ -36,6 +38,7 @@ public interface DeviceCoordinator { /** * Returns the kind of device type this coordinator supports. + * * @return */ DeviceType getDeviceType(); @@ -43,6 +46,7 @@ public interface DeviceCoordinator { /** * Returns the Activity class to be started in order to perform a pairing of a * given device. + * * @return */ Class getPairingActivity(); @@ -50,6 +54,7 @@ public interface DeviceCoordinator { /** * Returns the Activity class that will be used as the primary activity * for the given device. + * * @return */ Class getPrimaryActivity(); @@ -57,6 +62,7 @@ public interface DeviceCoordinator { /** * Returns true if activity data fetching is supported by the device * (with this coordinator). + * * @return */ boolean supportsActivityDataFetching(); @@ -65,6 +71,7 @@ public interface DeviceCoordinator { * Returns true if activity data fetching is supported AND possible at this * very moment. This will consider the device state (being connected/disconnected/busy...) * etc. + * * @param device * @return */ @@ -72,6 +79,7 @@ public interface DeviceCoordinator { /** * Returns the sample provider for the device being supported. + * * @return */ SampleProvider getSampleProvider(); @@ -79,6 +87,7 @@ public interface DeviceCoordinator { /** * Finds an install handler for the given uri that can install the given * uri on the device being managed. + * * @param uri * @param context * @return the install handler or null if that uri cannot be installed on the device @@ -87,7 +96,17 @@ public interface DeviceCoordinator { /** * Returns true if this device/coordinator supports taking screenshots. + * * @return */ boolean supportsScreenshots(); + + /** + * Returns true if this device/coordinator supports settig alarms. + * + * @return + */ + boolean supportsAlarmConfiguration(); + + int getTapString(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java index 6d7bf8533..a6dd21bf1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java @@ -1,14 +1,14 @@ package nodomain.freeyourgadget.gadgetbridge.devices; import android.net.Uri; -import android.support.annotation.Nullable; import java.util.ArrayList; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; -import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; /** * Specifies all events that GadgetBridge intends to send to the gadget device. @@ -22,9 +22,9 @@ public interface EventHandler { void onSetAlarms(ArrayList alarms); - void onSetCallState(@Nullable String number, @Nullable String name, ServiceCommand command); + void onSetCallState(CallSpec callSpec); - void onSetMusicInfo(String artist, String album, String track); + void onSetMusicInfo(MusicSpec musicSpec); void onEnableRealtimeSteps(boolean enable); @@ -36,11 +36,19 @@ public interface EventHandler { void onAppDelete(UUID uuid); + void onAppConfiguration(UUID appUuid, String config); + void onFetchActivityData(); void onReboot(); + void onHeartRateTest(); + + void onEnableRealtimeHeartRateMeasurement(boolean enable); + void onFindDevice(boolean start); void onScreenshotReq(); + + void onEnableHeartRateSleepSupport(boolean enable); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/InstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/InstallHandler.java index 1314dc0e0..e83815e62 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/InstallHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/InstallHandler.java @@ -28,7 +28,7 @@ public interface InstallHandler { void validateInstallation(InstallActivity installActivity, GBDevice device); /** - * Allows device specivic code to be execute just before the installation starts + * Allows device specific code to be executed just before the installation starts */ void onStartInstall(GBDevice device); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java index 5ce64b901..78463d029 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java @@ -1,18 +1,19 @@ package nodomain.freeyourgadget.gadgetbridge.devices; public interface SampleProvider { - byte PROVIDER_MIBAND = 0; - byte PROVIDER_PEBBLE_MORPHEUZ = 1; - byte PROVIDER_PEBBLE_GADGETBRIDGE = 2; - byte PROVIDER_PEBBLE_MISFIT = 3; + int PROVIDER_MIBAND = 0; + int PROVIDER_PEBBLE_MORPHEUZ = 1; + int PROVIDER_PEBBLE_GADGETBRIDGE = 2; + int PROVIDER_PEBBLE_MISFIT = 3; + int PROVIDER_PEBBLE_HEALTH = 4; - byte PROVIDER_UNKNOWN = 100; + int PROVIDER_UNKNOWN = 100; - int normalizeType(byte rawType); + int normalizeType(int rawType); - byte toRawActivityKind(int activityKind); + int toRawActivityKind(int activityKind); - float normalizeIntensity(short rawIntensity); + float normalizeIntensity(int rawIntensity); - byte getID(); + int getID(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java index ce690f7cd..1af8da27e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java @@ -15,22 +15,22 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator { private static final class UnknownSampleProvider implements SampleProvider { @Override - public int normalizeType(byte rawType) { + public int normalizeType(int rawType) { return ActivityKind.TYPE_UNKNOWN; } @Override - public byte toRawActivityKind(int activityKind) { + public int toRawActivityKind(int activityKind) { return 0; } @Override - public float normalizeIntensity(short rawIntensity) { + public float normalizeIntensity(int rawIntensity) { return 0; } @Override - public byte getID() { + public int getID() { return PROVIDER_UNKNOWN; } } @@ -83,4 +83,14 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator { public boolean supportsScreenshots() { return false; } + + @Override + public boolean supportsAlarmConfiguration() { + return false; + } + + @Override + public int getTapString() { + return 0; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java index 200616743..8379d75bd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java @@ -1,24 +1,21 @@ package nodomain.freeyourgadget.gadgetbridge.devices.miband; -import android.content.SharedPreferences; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + public final class MiBandConst { private static final Logger LOG = LoggerFactory.getLogger(MiBandConst.class); public static final String PREF_USER_ALIAS = "mi_user_alias"; - public static final String PREF_USER_YEAR_OF_BIRTH = "mi_user_year_of_birth"; - public static final String PREF_USER_GENDER = "mi_user_gender"; - public static final String PREF_USER_HEIGHT_CM = "mi_user_height_cm"; - public static final String PREF_USER_WEIGHT_KG = "mi_user_weight_kg"; public static final String PREF_MIBAND_WEARSIDE = "mi_wearside"; public static final String PREF_MIBAND_ADDRESS = "development_miaddr"; // FIXME: should be prefixed mi_ public static final String PREF_MIBAND_ALARMS = "mi_alarms"; public static final String PREF_MIBAND_FITNESS_GOAL = "mi_fitness_goal"; public static final String PREF_MIBAND_DONT_ACK_TRANSFER = "mi_dont_ack_transfer"; 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 ORIGIN_SMS = "sms"; @@ -29,20 +26,14 @@ public final class MiBandConst { public static final String MI_1 = "1"; public static final String MI_1A = "1A"; public static final String MI_1S = "1S"; + public static final String MI_AMAZFIT = "Amazfit"; - public static int getNotificationPrefIntValue(String pref, String origin, SharedPreferences prefs, int defaultValue) { + public static int getNotificationPrefIntValue(String pref, String origin, Prefs prefs, int defaultValue) { String key = getNotificationPrefKey(pref, origin); - String value = null; - try { - value = prefs.getString(key, String.valueOf(defaultValue)); - return Integer.valueOf(value); - } catch (NumberFormatException ex) { - LOG.error("Error converting preference value to int: " + key + ": " + value); - return defaultValue; - } + return prefs.getInt(key, defaultValue); } - public static String getNotificationPrefStringValue(String pref, String origin, SharedPreferences prefs, String defaultValue) { + public static String getNotificationPrefStringValue(String pref, String origin, Prefs prefs, String defaultValue) { String key = getNotificationPrefKey(pref, origin); return prefs.getString(key, defaultValue); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java index 7b3af1b8f..c4e8e83c0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java @@ -2,23 +2,22 @@ package nodomain.freeyourgadget.gadgetbridge.devices.miband; import android.app.Activity; import android.content.Context; -import android.content.SharedPreferences; import android.net.Uri; -import android.preference.PreferenceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Calendar; - import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class MiBandCoordinator extends AbstractDeviceCoordinator { private static final Logger LOG = LoggerFactory.getLogger(MiBandCoordinator.class); @@ -75,6 +74,16 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator { return false; } + @Override + public boolean supportsAlarmConfiguration() { + return true; + } + + @Override + public int getTapString() { + return R.string.tap_connected_device_for_activity; + } + public static boolean hasValidUserInfo() { String dummyMacAddress = MiBandService.MAC_ADDRESS_FILTER_1_1A + ":00:00:00"; try { @@ -107,22 +116,16 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator { * @throws IllegalArgumentException when the user info can not be created */ public static UserInfo getConfiguredUserInfo(String miBandAddress) throws IllegalArgumentException { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(GBApplication.getContext()); - int userYear = Integer.parseInt(prefs.getString(MiBandConst.PREF_USER_YEAR_OF_BIRTH, "0")); - int age = 25; - if (userYear > 1900) { - age = Calendar.getInstance().get(Calendar.YEAR) - userYear; - if (age <= 0) { - age = 25; - } - } + ActivityUser activityUser = new ActivityUser(); + Prefs prefs = GBApplication.getPrefs(); + UserInfo info = UserInfo.create( miBandAddress, prefs.getString(MiBandConst.PREF_USER_ALIAS, null), - ("male".equals(prefs.getString(MiBandConst.PREF_USER_GENDER, null)) ? 1 : 0), - age, - Integer.parseInt(prefs.getString(MiBandConst.PREF_USER_HEIGHT_CM, "175")), - Integer.parseInt(prefs.getString(MiBandConst.PREF_USER_WEIGHT_KG, "70")), + activityUser.getActivityUserGender(), + activityUser.getActivityUserAge(), + activityUser.getActivityUserHeightCm(), + activityUser.getActivityUserWeightKg(), 0 ); return info; @@ -130,20 +133,25 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator { public static int getWearLocation(String miBandAddress) throws IllegalArgumentException { int location = 0; //left hand - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(GBApplication.getContext()); + Prefs prefs = GBApplication.getPrefs(); if ("right".equals(prefs.getString(MiBandConst.PREF_MIBAND_WEARSIDE, "left"))) { location = 1; // right hand } return location; } + public static boolean getHeartrateSleepSupport(String miBandAddress) throws IllegalArgumentException { + Prefs prefs = GBApplication.getPrefs(); + return prefs.getBoolean(MiBandConst.PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION, false); + } + public static int getFitnessGoal(String miBandAddress) throws IllegalArgumentException { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(GBApplication.getContext()); - return Integer.parseInt(prefs.getString(MiBandConst.PREF_MIBAND_FITNESS_GOAL, "10000")); + Prefs prefs = GBApplication.getPrefs(); + return prefs.getInt(MiBandConst.PREF_MIBAND_FITNESS_GOAL, 10000); } public static int getReservedAlarmSlots(String miBandAddress) throws IllegalArgumentException { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(GBApplication.getContext()); - return Integer.parseInt(prefs.getString(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, "0")); + Prefs prefs = GBApplication.getPrefs(); + return prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java index 8485af7f2..20ff39d92 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java @@ -1,8 +1,8 @@ package nodomain.freeyourgadget.gadgetbridge.devices.miband; -import android.content.ContentResolver; import android.content.Context; import android.net.Uri; +import android.support.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,22 +10,28 @@ import org.slf4j.LoggerFactory; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.Locale; +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 { private static final Logger LOG = LoggerFactory.getLogger(MiBandFWHelper.class); - private final Uri uri; - private final ContentResolver cr; - private byte[] fw; - - private final int offsetFirmwareVersionBuild = 1056; - private final int offsetFirmwareVersionRevision = 1057; - private final int offsetFirmwareVersionMinor = 1058; - private final int offsetFirmwareVersionMajor = 1059; + /** + * The backing firmware info instance, which in general supports the provided + * given firmware. You must call AbstractMiFirmwareInfo#checkValid() before + * attempting to flash it. + */ + @NonNull + private final AbstractMiFirmwareInfo firmwareInfo; + @NonNull + private final byte[] fw; /** * Provides a different notification API which is also used on Mi1A devices. @@ -45,54 +51,56 @@ public class MiBandFWHelper { }; public MiBandFWHelper(Uri uri, Context context) throws IOException { - this.uri = uri; - cr = context.getContentResolver(); - if (cr == null) { - throw new IOException("No content resolver"); - } - 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(cr.openInputStream(uri))) { + try (InputStream in = new BufferedInputStream(context.getContentResolver().openInputStream(uri))) { this.fw = FileUtils.readAll(in, 1024 * 1024); // 1 MB - if (fw.length <= offsetFirmwareVersionMajor) { - throw new IOException("This doesn't seem to be a Mi Band firmware, file size too small."); - } - byte firmwareVersionMajor = fw[offsetFirmwareVersionMajor]; - if (!isSupportedFirmwareVersionMajor(firmwareVersionMajor)) { - throw new IOException("Firmware major version not supported, either too new or this isn't a Mi Band firmware: " + firmwareVersionMajor); - } + 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); } } - private byte getFirmwareVersionMajor() { - return fw[offsetFirmwareVersionMajor]; - } - - private byte getFirmwareVersionMinor() { - return fw[offsetFirmwareVersionMinor]; - } - - private boolean isSupportedFirmwareVersionMajor(byte firmwareVersionMajor) { - return firmwareVersionMajor == 1 || firmwareVersionMajor == 5; - } - public int getFirmwareVersion() { - return (fw[offsetFirmwareVersionMajor] << 24) | (fw[offsetFirmwareVersionMinor] << 16) | (fw[offsetFirmwareVersionRevision] << 8) | fw[offsetFirmwareVersionBuild]; + // FIXME: UnsupportedOperationException! + return firmwareInfo.getFirst().getFirmwareVersion(); + } + + 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 String.format(Locale.US, "%d.%d.%d.%d", fw[offsetFirmwareVersionMajor], fw[offsetFirmwareVersionMinor], fw[offsetFirmwareVersionRevision], fw[offsetFirmwareVersionBuild]); + return format(getFirmwareVersion()); } + public String getHumanFirmwareVersion2() { + return format(firmwareInfo.getSecond().getFirmwareVersion()); + } + + public String format(int version) { + return formatFirmwareVersion(version); + } + + @NonNull public byte[] getFw() { return fw; } @@ -107,13 +115,31 @@ public class MiBandFWHelper { } public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) { - String deviceHW = device.getHardwareVersion(); - if (MiBandConst.MI_1.equals(deviceHW)) { - return getFirmwareVersionMajor() == 1; - } - if (MiBandConst.MI_1A.equals(deviceHW)) { - return getFirmwareVersionMajor() == 5; - } - return false; + return firmwareInfo.isGenerallyCompatibleWith(device); + } + + public boolean isSingleFirmware() { + return firmwareInfo.isSingleMiBandFirmware(); + } + + /** + * @param wholeFirmwareBytes + * @return + * @throws IllegalArgumentException when the data is not recognized as firmware data + */ + public static + @NonNull + AbstractMiFirmwareInfo determineFirmwareInfoFor(byte[] wholeFirmwareBytes) { + return AbstractMiFirmwareInfo.determineFirmwareInfoFor(wholeFirmwareBytes); + } + + /** + * The backing firmware info instance, which in general supports the provided + * given firmware. You MUST call AbstractMiFirmwareInfo#checkValid() AND + * isGenerallyCompatibleWithDevice() before attempting to flash it. + */ + @NonNull + public AbstractMiFirmwareInfo getFirmwareInfo() { + return firmwareInfo; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWInstallHandler.java index 644b94d38..8fcc91a4b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWInstallHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWInstallHandler.java @@ -47,6 +47,14 @@ public class MiBandFWInstallHandler implements InstallHandler { 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); @@ -56,7 +64,13 @@ public class MiBandFWInstallHandler implements InstallHandler { installActivity.setInstallEnabled(false); return; } - StringBuilder builder = new StringBuilder(mContext.getString(R.string.fw_upgrade_notice, helper.getHumanFirmwareVersion())); + 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)); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java index c33a1909e..12d886836 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java @@ -7,11 +7,9 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.widget.TextView; import android.widget.Toast; @@ -26,6 +24,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class MiBandPairingActivity extends Activity { private static final Logger LOG = LoggerFactory.getLogger(MiBandPairingActivity.class); @@ -159,13 +158,19 @@ public class MiBandPairingActivity extends Activity { } 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) { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - sharedPrefs.edit().putString(MiBandConst.PREF_MIBAND_ADDRESS, macAddress).apply(); + Prefs prefs = GBApplication.getPrefs(); + prefs.getPreferences().edit().putString(MiBandConst.PREF_MIBAND_ADDRESS, macAddress).apply(); } Intent intent = new Intent(this, ControlCenter.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); @@ -188,12 +193,13 @@ public class MiBandPairingActivity extends Activity { bondingMacAddress = device.getAddress(); if (bondState == BluetoothDevice.BOND_BONDING) { - LOG.info("Bonding in progress: " + device.getAddress()); + GB.toast(this, "Bonding in progress: " + bondingMacAddress, Toast.LENGTH_LONG, GB.INFO); return; } + GB.toast(this, "Creating bond with" + bondingMacAddress, Toast.LENGTH_LONG, GB.INFO); if (!device.createBond()) { - GB.toast(this, "Unable to pair with " + device.getAddress(), Toast.LENGTH_LONG, GB.ERROR); + GB.toast(this, "Unable to pair with " + bondingMacAddress, Toast.LENGTH_LONG, GB.ERROR); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java index 62a105269..e49ae1abb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java @@ -5,6 +5,7 @@ import android.os.Bundle; import android.preference.Preference; import android.support.v4.content.LocalBroadcastManager; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivity; import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter; @@ -18,12 +19,9 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PR import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_DONT_ACK_TRANSFER; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_FITNESS_GOAL; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_WEARSIDE; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_USER_ALIAS; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_USER_GENDER; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_USER_HEIGHT_CM; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_USER_WEIGHT_KG; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_USER_YEAR_OF_BIRTH; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PROFILE; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefKey; @@ -47,30 +45,27 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity { }); + final Preference enableHeartrateSleepSupport = findPreference(PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION); + enableHeartrateSleepSupport.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + GBApplication.deviceService().onEnableHeartRateSleepSupport(Boolean.TRUE.equals(newVal)); + return true; + } + }); } @Override protected String[] getPreferenceKeysWithSummary() { return new String[]{ PREF_USER_ALIAS, - PREF_USER_YEAR_OF_BIRTH, - PREF_USER_GENDER, - PREF_USER_HEIGHT_CM, - PREF_USER_WEIGHT_KG, - PREF_MIBAND_WEARSIDE, PREF_MIBAND_ADDRESS, PREF_MIBAND_FITNESS_GOAL, - PREF_MIBAND_DONT_ACK_TRANSFER, PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, - getNotificationPrefKey(VIBRATION_PROFILE, ORIGIN_SMS), getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_SMS), - getNotificationPrefKey(VIBRATION_PROFILE, ORIGIN_INCOMING_CALL), getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_INCOMING_CALL), - getNotificationPrefKey(VIBRATION_PROFILE, ORIGIN_K9MAIL), getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_K9MAIL), - getNotificationPrefKey(VIBRATION_PROFILE, ORIGIN_PEBBLEMSG), getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_PEBBLEMSG), - getNotificationPrefKey(VIBRATION_PROFILE, ORIGIN_GENERIC), getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_GENERIC), }; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java index 95026cb07..b87406e17 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java @@ -4,12 +4,12 @@ import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; public class MiBandSampleProvider implements SampleProvider { - public static final byte TYPE_DEEP_SLEEP = 5; - public static final byte TYPE_LIGHT_SLEEP = 4; - public static final byte TYPE_ACTIVITY = -1; - public static final byte TYPE_UNKNOWN = -1; - public static final byte TYPE_NONWEAR = 3; - public static final byte TYPE_CHARGING = 6; + public static final int TYPE_DEEP_SLEEP = 5; + public static final int TYPE_LIGHT_SLEEP = 4; + 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; // public static final byte TYPE_NREM = 5; // DEEP SLEEP // public static final byte TYPE_ONBED = 7; @@ -23,7 +23,7 @@ public class MiBandSampleProvider implements SampleProvider { private final float movementDivisor = 180.0f; //256.0f; @Override - public int normalizeType(byte rawType) { + public int normalizeType(int rawType) { switch (rawType) { case TYPE_DEEP_SLEEP: return ActivityKind.TYPE_DEEP_SLEEP; @@ -42,7 +42,7 @@ public class MiBandSampleProvider implements SampleProvider { } @Override - public byte toRawActivityKind(int activityKind) { + public int toRawActivityKind(int activityKind) { switch (activityKind) { case ActivityKind.TYPE_ACTIVITY: return TYPE_ACTIVITY; @@ -59,12 +59,12 @@ public class MiBandSampleProvider implements SampleProvider { } @Override - public float normalizeIntensity(short rawIntensity) { + public float normalizeIntensity(int rawIntensity) { return rawIntensity / movementDivisor; } @Override - public byte getID() { + public int getID() { return SampleProvider.PROVIDER_MIBAND; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java index 349c0f481..39d5bd69f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java @@ -14,6 +14,8 @@ public class MiBandService { public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0")); + public static final UUID UUID_SERVICE_HEART_RATE = UUID.fromString(String.format(BASE_UUID, "180D")); + 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")); @@ -44,6 +46,11 @@ public class MiBandService { 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 String UUID_SERVICE_WEIGHT_SERVICE = "00001530-0000-3512-2118-0009af100700"; @@ -153,6 +160,12 @@ public class MiBandService { 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 @@ -213,6 +226,7 @@ public class MiBandService { static { MIBAND_DEBUG = new HashMap<>(); MIBAND_DEBUG.put(UUID_SERVICE_MIBAND_SERVICE, "MiBand Service"); + MIBAND_DEBUG.put(UUID_SERVICE_HEART_RATE, "MiBand HR Service"); MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DEVICE_INFO, "Device Info"); MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DEVICE_NAME, "Device Name"); @@ -229,6 +243,8 @@ public class MiBandService { MIBAND_DEBUG.put(UUID_CHARACTERISTIC_TEST, "Test"); MIBAND_DEBUG.put(UUID_CHARACTERISTIC_SENSOR_DATA, "Sensor Data"); MIBAND_DEBUG.put(UUID_CHARACTERISTIC_PAIR, "Pair"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT, "Heart Rate Control Point"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT, "Heart Rate Measure"); } public static String lookup(UUID uuid, String fallback) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/UserInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/UserInfo.java index 48439da7d..4bc728c4c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/UserInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/UserInfo.java @@ -1,10 +1,11 @@ package nodomain.freeyourgadget.gadgetbridge.devices.miband; +import java.util.Arrays; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.DeviceInfo; import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; -import java.util.Arrays; - public class UserInfo { private final String btAddress; @@ -23,7 +24,7 @@ public class UserInfo { * @param btAddress the address of the MI Band to connect to. */ public static UserInfo getDefault(String btAddress) { - return new UserInfo(btAddress, "1550050550", 0, 25, 175, 70, 0); + return new UserInfo(btAddress, "1550050550", ActivityUser.defaultUserGender, ActivityUser.defaultUserAge, ActivityUser.defaultUserHeightCm, ActivityUser.defaultUserWeightKg, 0); } /** @@ -84,13 +85,13 @@ public class UserInfo { sequence[8] = (byte) (type & 0xff); int aliasFrom = 9; - if (mDeviceInfo.isMili1A() || mDeviceInfo.isMilli1S()) { + if (!mDeviceInfo.isMili1()) { sequence[9] = (byte) (mDeviceInfo.feature & 255); sequence[10] = (byte) (mDeviceInfo.appearance & 255); aliasFrom = 11; } - byte[] aliasBytes = alias.substring(0, Math.min(alias.length(), 19-aliasFrom)).getBytes(); + byte[] aliasBytes = alias.substring(0, Math.min(alias.length(), 19 - aliasFrom)).getBytes(); System.arraycopy(aliasBytes, 0, sequence, aliasFrom, aliasBytes.length); byte[] crcSequence = Arrays.copyOf(sequence, 19); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java new file mode 100644 index 000000000..b62abe167 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java @@ -0,0 +1,52 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.pebble; + +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public class HealthSampleProvider implements SampleProvider { + public static final int TYPE_DEEP_SLEEP = 5; + public static final int TYPE_LIGHT_SLEEP = 4; + public static final int TYPE_ACTIVITY = -1; + + protected final float movementDivisor = 8000f; + + @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: + default: + 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_UNKNOWN: // fall through + default: + return TYPE_ACTIVITY; + } + } + + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity / movementDivisor; + } + + + @Override + public int getID() { + return SampleProvider.PROVIDER_PEBBLE_HEALTH; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MisfitSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MisfitSampleProvider.java index 608f97712..bce42a375 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MisfitSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MisfitSampleProvider.java @@ -7,24 +7,24 @@ public class MisfitSampleProvider implements SampleProvider { protected final float movementDivisor = 300f; @Override - public int normalizeType(byte rawType) { + public int normalizeType(int rawType) { return (int) rawType; } @Override - public byte toRawActivityKind(int activityKind) { + public int toRawActivityKind(int activityKind) { return (byte) activityKind; } @Override - public float normalizeIntensity(short rawIntensity) { + public float normalizeIntensity(int rawIntensity) { return rawIntensity / movementDivisor; } @Override - public byte getID() { + public int getID() { return SampleProvider.PROVIDER_PEBBLE_MISFIT; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MorpheuzSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MorpheuzSampleProvider.java index e9bdf7a07..1867c3413 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MorpheuzSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MorpheuzSampleProvider.java @@ -5,15 +5,15 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; public class MorpheuzSampleProvider implements SampleProvider { // raw types - public static final byte TYPE_DEEP_SLEEP = 5; - public static final byte TYPE_LIGHT_SLEEP = 4; - public static final byte TYPE_ACTIVITY = -1; - public static final byte TYPE_UNKNOWN = -1; + public static final int TYPE_DEEP_SLEEP = 5; + public static final int TYPE_LIGHT_SLEEP = 4; + public static final int TYPE_ACTIVITY = -1; + public static final int TYPE_UNKNOWN = -1; protected float movementDivisor = 5000f; @Override - public int normalizeType(byte rawType) { + public int normalizeType(int rawType) { switch (rawType) { case TYPE_DEEP_SLEEP: return ActivityKind.TYPE_DEEP_SLEEP; @@ -28,7 +28,7 @@ public class MorpheuzSampleProvider implements SampleProvider { } @Override - public byte toRawActivityKind(int activityKind) { + public int toRawActivityKind(int activityKind) { switch (activityKind) { case ActivityKind.TYPE_ACTIVITY: return TYPE_ACTIVITY; @@ -43,12 +43,12 @@ public class MorpheuzSampleProvider implements SampleProvider { } @Override - public float normalizeIntensity(short rawIntensity) { + public float normalizeIntensity(int rawIntensity) { return rawIntensity / movementDivisor; } @Override - public byte getID() { + public int getID() { return SampleProvider.PROVIDER_PEBBLE_MORPHEUZ; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java index 1619467b8..30becd117 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java @@ -3,6 +3,8 @@ package nodomain.freeyourgadget.gadgetbridge.devices.pebble; import android.content.Context; import android.net.Uri; +import org.json.JSONException; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,6 +13,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStream; import java.io.Writer; import nodomain.freeyourgadget.gadgetbridge.R; @@ -21,6 +24,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; +import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; public class PBWInstallHandler implements InstallHandler { private static final Logger LOG = LoggerFactory.getLogger(PBWInstallHandler.class); @@ -47,15 +51,7 @@ public class PBWInstallHandler implements InstallHandler { return; } - String hwRev = device.getHardwareVersion(); - String platformName; - if (hwRev.startsWith("snowy")) { - platformName = "basalt"; - } else if (hwRev.startsWith("spalding")) { - platformName = "chalk"; - } else { - platformName = "aplite"; - } + String platformName = PebbleUtils.getPlatformName(device.getHardwareVersion()); try { mPBWReader = new PBWReader(mUri, mContext, platformName); @@ -158,10 +154,29 @@ public class PBWInstallHandler implements InstallHandler { } try { LOG.info(app.getJSON().toString()); - writer.write(app.getJSON().toString()); + JSONObject appJSON = app.getJSON(); + JSONObject appKeysJSON = mPBWReader.getAppKeysJSON(); + if (appKeysJSON != null) { + appJSON.put("appKeys", appKeysJSON); + } + writer.write(appJSON.toString()); + writer.close(); } catch (IOException e) { LOG.error("Failed to write to output file: " + e.getMessage(), e); + } catch (JSONException e) { + LOG.error(e.getMessage(), e); + } + + InputStream jsConfigFile = mPBWReader.getInputStreamFile("pebble-js-app.js"); + + if (jsConfigFile != null) { + outputFile = new File(destDir, app.getUUID().toString() + "_config.js"); + try { + FileUtils.copyStreamToFile(jsConfigFile, outputFile); + } catch (IOException e) { + LOG.error("Failed to open output file: " + e.getMessage(), e); + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java index ad8f6e118..42f05b745 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java @@ -58,6 +58,8 @@ public class PBWReader { private int mIconId; private int mFlags; + private JSONObject mAppKeys = null; + public PBWReader(Uri uri, Context context, String platform) throws FileNotFoundException { this.uri = uri; cr = context.getContentResolver(); @@ -201,6 +203,10 @@ public class PBWReader { appCreator = json.getString("companyName"); appVersion = json.getString("versionLabel"); appUUID = UUID.fromString(json.getString("uuid")); + if (json.has("appKeys")) { + mAppKeys = json.getJSONObject("appKeys"); + LOG.info("found appKeys:" + mAppKeys.toString()); + } } catch (JSONException e) { isValid = false; e.printStackTrace(); @@ -317,4 +323,8 @@ public class PBWReader { public int getIconId() { return mIconId; } + + public JSONObject getAppKeysJSON() { + return mAppKeys; + } } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java index 80450f722..c74db0541 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java @@ -2,11 +2,10 @@ package nodomain.freeyourgadget.gadgetbridge.devices.pebble; import android.app.Activity; import android.content.Context; -import android.content.SharedPreferences; import android.net.Uri; -import android.preference.PreferenceManager; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AppManagerActivity; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; @@ -14,6 +13,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; 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 PebbleCoordinator extends AbstractDeviceCoordinator { public PebbleCoordinator() { @@ -45,13 +45,19 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator { @Override public SampleProvider getSampleProvider() { - // FIXME: make this configurable somewhere else. - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(GBApplication.getContext()); - if (sharedPrefs.getBoolean("pebble_force_untested", false)) { - //return new PebbleGadgetBridgeSampleProvider(); - return new MisfitSampleProvider(); - } else { - return new MorpheuzSampleProvider(); + Prefs prefs = GBApplication.getPrefs(); + int activityTracker = prefs.getInt("pebble_activitytracker", SampleProvider.PROVIDER_PEBBLE_HEALTH); + switch (activityTracker) { + case SampleProvider.PROVIDER_PEBBLE_HEALTH: + return new HealthSampleProvider(); + case SampleProvider.PROVIDER_PEBBLE_MISFIT: + return new MisfitSampleProvider(); + case SampleProvider.PROVIDER_PEBBLE_MORPHEUZ: + return new MorpheuzSampleProvider(); + case SampleProvider.PROVIDER_PEBBLE_GADGETBRIDGE: + return new PebbleGadgetBridgeSampleProvider(); + default: + return new HealthSampleProvider(); } } @@ -70,4 +76,14 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator { public boolean supportsScreenshots() { return true; } + + @Override + public boolean supportsAlarmConfiguration() { + return false; + } + + @Override + public int getTapString() { + return R.string.tap_connected_device_for_app_mananger; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleGadgetBridgeSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleGadgetBridgeSampleProvider.java index 028201ac3..b047fdb42 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleGadgetBridgeSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleGadgetBridgeSampleProvider.java @@ -8,7 +8,7 @@ public class PebbleGadgetBridgeSampleProvider extends MorpheuzSampleProvider { } @Override - public byte getID() { + public int getID() { return SampleProvider.PROVIDER_PEBBLE_GADGETBRIDGE; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java index 74a3d2dcf..73d26d0d4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java @@ -4,12 +4,11 @@ import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class BluetoothStateChangeReceiver extends BroadcastReceiver { @Override @@ -22,8 +21,8 @@ public class BluetoothStateChangeReceiver extends BroadcastReceiver { Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST); LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent); - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); - if (!sharedPrefs.getBoolean("general_autoconnectonbluetooth", false)) { + Prefs prefs = GBApplication.getPrefs(); + if (!prefs.getBoolean("general_autoconnectonbluetooth", false)) { return; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java index 1949b1ac7..899ee0c6a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java @@ -3,11 +3,9 @@ package nodomain.freeyourgadget.gadgetbridge.externalevents; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.PowerManager; -import android.preference.PreferenceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,6 +13,7 @@ import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class K9Receiver extends BroadcastReceiver { @@ -24,11 +23,11 @@ public class K9Receiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); - if ("never".equals(sharedPrefs.getString("notification_mode_k9mail", "when_screen_off"))) { + Prefs prefs = GBApplication.getPrefs(); + if ("never".equals(prefs.getString("notification_mode_k9mail", "when_screen_off"))) { return; } - if ("when_screen_off".equals(sharedPrefs.getString("notification_mode_k9mail", "when_screen_off"))) { + if ("when_screen_off".equals(prefs.getString("notification_mode_k9mail", "when_screen_off"))) { PowerManager powermanager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); if (powermanager.isScreenOn()) { return; @@ -55,20 +54,9 @@ public class K9Receiver extends BroadcastReceiver { * It should be the first one returned by the query in most cases, */ - Cursor c = null; - try { - c = context.getContentResolver().query(k9Uri, messagesProjection, null, null, null); - } catch (Exception e) { - e.printStackTrace(); - notificationSpec.sender = "Gadgetbridge"; - notificationSpec.subject = "Permission Error?"; - notificationSpec.body = "Please reinstall Gadgerbridge to enable K-9 Mail notifications"; - } - - try { + try (Cursor c = context.getContentResolver().query(k9Uri, messagesProjection, null, null, null)) { if (c != null) { - c.moveToFirst(); - do { + while (c.moveToNext()) { String uri = c.getString(c.getColumnIndex("uri")); if (uri.equals(uriWanted)) { notificationSpec.sender = c.getString(c.getColumnIndex("senderAddress")); @@ -76,12 +64,13 @@ public class K9Receiver extends BroadcastReceiver { notificationSpec.body = c.getString(c.getColumnIndex("preview")); break; } - } while (c.moveToNext()); - } - } finally { - if (c != null) { - c.close(); + } } + } catch (Exception e) { + e.printStackTrace(); + notificationSpec.sender = "Gadgetbridge"; + notificationSpec.subject = "Permission Error?"; + notificationSpec.body = "Please reinstall Gadgetbridge to enable K-9 Mail notifications"; } GBApplication.deviceService().onNotification(notificationSpec); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java index 371e33d44..d910e75eb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java @@ -8,20 +8,31 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; public class MusicPlaybackReceiver extends BroadcastReceiver { private static final Logger LOG = LoggerFactory.getLogger(MusicPlaybackReceiver.class); - private static String mLastSource; - @Override public void onReceive(Context context, Intent intent) { String artist = intent.getStringExtra("artist"); String album = intent.getStringExtra("album"); String track = intent.getStringExtra("track"); - + /* + Bundle bundle = intent.getExtras(); + for (String key : bundle.keySet()) { + Object value = bundle.get(key); + LOG.info(String.format("%s %s (%s)", key, + value != null ? value.toString() : "null", value != null ? value.getClass().getName() : "no class")); + } + */ LOG.info("Current track: " + artist + ", " + album + ", " + track); - GBApplication.deviceService().onSetMusicInfo(artist, album, track); + MusicSpec musicSpec = new MusicSpec(); + musicSpec.artist = artist; + musicSpec.artist = album; + musicSpec.artist = track; + + GBApplication.deviceService().onSetMusicInfo(musicSpec); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index d4f6ed63d..9f93618b2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -8,12 +8,10 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.PowerManager; -import android.preference.PreferenceManager; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.support.v4.app.NotificationCompat; @@ -30,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class NotificationListener extends NotificationListenerService { @@ -54,6 +53,9 @@ public class NotificationListener extends NotificationListenerService { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); switch (action) { + case GBApplication.ACTION_QUIT: + stopSelf(); + break; case ACTION_MUTE: case ACTION_OPEN: { StatusBarNotification[] sbns = NotificationListener.this.getActiveNotifications(); @@ -130,6 +132,7 @@ public class NotificationListener extends NotificationListenerService { public void onCreate() { super.onCreate(); IntentFilter filterLocal = new IntentFilter(); + filterLocal.addAction(GBApplication.ACTION_QUIT); filterLocal.addAction(ACTION_OPEN); filterLocal.addAction(ACTION_DISMISS); filterLocal.addAction(ACTION_DISMISS_ALL); @@ -157,8 +160,8 @@ public class NotificationListener extends NotificationListenerService { return; } - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - if (!sharedPrefs.getBoolean("notifications_generic_whenscreenon", false)) { + Prefs prefs = GBApplication.getPrefs(); + if (!prefs.getBoolean("notifications_generic_whenscreenon", false)) { PowerManager powermanager = (PowerManager) getSystemService(POWER_SERVICE); if (powermanager.isScreenOn()) { return; @@ -185,13 +188,13 @@ public class NotificationListener extends NotificationListenerService { } if (source.equals("eu.siacs.conversations")) { - if (!"never".equals(sharedPrefs.getString("notification_mode_pebblemsg", "when_screen_off"))) { + if (!"never".equals(prefs.getString("notification_mode_pebblemsg", "when_screen_off"))) { return; } } if (source.equals("com.fsck.k9")) { - if (!"never".equals(sharedPrefs.getString("notification_mode_k9mail", "when_screen_off"))) { + if (!"never".equals(prefs.getString("notification_mode_k9mail", "when_screen_off"))) { return; } } @@ -201,7 +204,7 @@ public class NotificationListener extends NotificationListenerService { source.equals("com.sonyericsson.conversations") || source.equals("com.android.messaging") || source.equals("org.smssecure.smssecure")) { - if (!"never".equals(sharedPrefs.getString("notification_mode_sms", "when_screen_off"))) { + if (!"never".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) { return; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java index 01dab88f6..801c151bb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java @@ -3,9 +3,7 @@ package nodomain.freeyourgadget.gadgetbridge.externalevents; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.os.PowerManager; -import android.preference.PreferenceManager; import org.json.JSONArray; import org.json.JSONException; @@ -15,6 +13,7 @@ import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class PebbleReceiver extends BroadcastReceiver { @@ -23,11 +22,11 @@ public class PebbleReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); - if ("never".equals(sharedPrefs.getString("notification_mode_pebblemsg", "when_screen_off"))) { + Prefs prefs = GBApplication.getPrefs(); + if ("never".equals(prefs.getString("notification_mode_pebblemsg", "when_screen_off"))) { return; } - if ("when_screen_off".equals(sharedPrefs.getString("notification_mode_pebblemsg", "when_screen_off"))) { + if ("when_screen_off".equals(prefs.getString("notification_mode_pebblemsg", "when_screen_off"))) { PowerManager powermanager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); if (powermanager.isScreenOn()) { return; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PhoneCallReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PhoneCallReceiver.java index c17829176..496872db3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PhoneCallReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PhoneCallReceiver.java @@ -3,12 +3,11 @@ package nodomain.freeyourgadget.gadgetbridge.externalevents; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; import android.telephony.TelephonyManager; import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class PhoneCallReceiver extends BroadcastReceiver { @@ -31,7 +30,6 @@ public class PhoneCallReceiver extends BroadcastReceiver { } else if (TelephonyManager.EXTRA_STATE_RINGING.equals(stateStr)) { state = TelephonyManager.CALL_STATE_RINGING; } - onCallStateChanged(context, state, number); } } @@ -41,34 +39,38 @@ public class PhoneCallReceiver extends BroadcastReceiver { return; } - ServiceCommand callCommand = null; + int callCommand = CallSpec.CALL_UNDEFINED; switch (state) { case TelephonyManager.CALL_STATE_RINGING: mSavedNumber = number; - callCommand = ServiceCommand.CALL_INCOMING; + callCommand = CallSpec.CALL_INCOMING; break; case TelephonyManager.CALL_STATE_OFFHOOK: if (mLastState == TelephonyManager.CALL_STATE_RINGING) { - callCommand = ServiceCommand.CALL_START; + callCommand = CallSpec.CALL_START; } else { - callCommand = ServiceCommand.CALL_OUTGOING; + callCommand = CallSpec.CALL_OUTGOING; + mSavedNumber = number; } break; case TelephonyManager.CALL_STATE_IDLE: if (mLastState == TelephonyManager.CALL_STATE_RINGING) { //missed call would be correct here - callCommand = ServiceCommand.CALL_END; + callCommand = CallSpec.CALL_END; } else { - callCommand = ServiceCommand.CALL_END; + callCommand = CallSpec.CALL_END; } break; } - if (callCommand != null) { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); - if ("never".equals(sharedPrefs.getString("notification_mode_calls", "always"))) { + if (callCommand != CallSpec.CALL_UNDEFINED) { + Prefs prefs = GBApplication.getPrefs(); + if ("never".equals(prefs.getString("notification_mode_calls", "always"))) { return; } - GBApplication.deviceService().onSetCallState(mSavedNumber, null, callCommand); + CallSpec callSpec = new CallSpec(); + callSpec.number = mSavedNumber; + callSpec.command = callCommand; + GBApplication.deviceService().onSetCallState(callSpec); } mLastState = state; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java index 664e44e9a..0404cbc22 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java @@ -3,28 +3,24 @@ package nodomain.freeyourgadget.gadgetbridge.externalevents; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; import android.os.PowerManager; -import android.preference.PreferenceManager; import android.telephony.SmsMessage; -import java.util.ArrayList; - import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class SMSReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); - if ("never".equals(sharedPrefs.getString("notification_mode_sms", "when_screen_off"))) { + Prefs prefs = GBApplication.getPrefs(); + if ("never".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) { return; } - if ("when_screen_off".equals(sharedPrefs.getString("notification_mode_sms", "when_screen_off"))) { + if ("when_screen_off".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) { PowerManager powermanager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); if (powermanager.isScreenOn()) { return; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/TimeChangeReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/TimeChangeReceiver.java index 82f0836b2..0f58c21ff 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/TimeChangeReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/TimeChangeReceiver.java @@ -3,8 +3,6 @@ package nodomain.freeyourgadget.gadgetbridge.externalevents; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +12,7 @@ import java.util.GregorianCalendar; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class TimeChangeReceiver extends BroadcastReceiver { @@ -22,10 +21,10 @@ public class TimeChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + Prefs prefs = GBApplication.getPrefs(); final String action = intent.getAction(); - if (sharedPrefs.getBoolean("datetime_synconconnect", true) && (action.equals(Intent.ACTION_TIME_CHANGED) || action.equals(Intent.ACTION_TIMEZONE_CHANGED))) { + if (prefs.getBoolean("datetime_synconconnect", true) && (action.equals(Intent.ACTION_TIME_CHANGED) || action.equals(Intent.ACTION_TIMEZONE_CHANGED))) { Date newTime = GregorianCalendar.getInstance().getTime(); LOG.info("Time or Timezone changed, syncing with device: " + DateTimeUtils.formatDate(newTime) + " (" + newTime.toGMTString() + "), " + intent.getAction()); GBApplication.deviceService().onSetTime(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBActivitySample.java index 1cb7416d6..895b903b6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBActivitySample.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBActivitySample.java @@ -2,32 +2,42 @@ package nodomain.freeyourgadget.gadgetbridge.impl; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; public class GBActivitySample implements ActivitySample { private final int timestamp; private final SampleProvider provider; - private final short intensity; - private final short steps; - private final byte type; + private final int intensity; + private final int steps; + private final int type; + private final int customValue; - public GBActivitySample(SampleProvider provider, int timestamp, short intensity, short steps, byte type) { + public GBActivitySample(SampleProvider provider, int timestamp, int intensity, int steps, int type) { + this(provider, timestamp, intensity, steps, type, 0); + } + + public GBActivitySample(SampleProvider provider, int timestamp, int intensity, int steps, int type, int customValue) { this.timestamp = timestamp; this.provider = provider; this.intensity = intensity; this.steps = steps; + this.customValue = customValue; this.type = type; validate(); } private void validate() { if (steps < 0) { - throw new IllegalArgumentException("steps must be > 0"); + throw new IllegalArgumentException("steps must be >= 0"); } if (intensity < 0) { - throw new IllegalArgumentException("intensity must be > 0"); + throw new IllegalArgumentException("intensity must be >= 0"); } if (timestamp < 0) { - throw new IllegalArgumentException("timestamp must be > 0"); + throw new IllegalArgumentException("timestamp must be >= 0"); + } + if (customValue < 0) { + throw new IllegalArgumentException("customValue must be >= 0"); } } @@ -42,7 +52,7 @@ public class GBActivitySample implements ActivitySample { } @Override - public short getRawIntensity() { + public int getRawIntensity() { return intensity; } @@ -52,12 +62,12 @@ public class GBActivitySample implements ActivitySample { } @Override - public short getSteps() { + public int getSteps() { return steps; } @Override - public byte getRawKind() { + public int getRawKind() { return type; } @@ -65,4 +75,20 @@ public class GBActivitySample implements ActivitySample { public int getKind() { return getProvider().normalizeType(getRawKind()); } + + @Override + public int getCustomValue() { + return customValue; + } + + @Override + public String toString() { + return "GBActivitySample{" + + "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimeStamp(timestamp)) + + ", intensity=" + getIntensity() + + ", steps=" + getSteps() + + ", customValue=" + getCustomValue() + + ", type=" + getKind() + + '}'; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java index d0178e4cf..546bcee22 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java @@ -1,8 +1,6 @@ package nodomain.freeyourgadget.gadgetbridge.impl; -import android.content.SharedPreferences; import android.os.Parcel; -import android.preference.PreferenceManager; import android.support.annotation.NonNull; import java.util.Calendar; @@ -12,6 +10,7 @@ import java.util.Set; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ALARMS; @@ -187,8 +186,8 @@ public class GBAlarm implements Alarm { } public void store() { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(GBApplication.getContext()); - Set preferencesAlarmListSet = sharedPrefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet()); + Prefs prefs = GBApplication.getPrefs(); + Set preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet()); //the old Set cannot be updated in place see http://developer.android.com/reference/android/content/SharedPreferences.html#getStringSet%28java.lang.String,%20java.util.Set%3Cjava.lang.String%3E%29 Set newPrefs = new HashSet<>(preferencesAlarmListSet); @@ -202,7 +201,7 @@ public class GBAlarm implements Alarm { } } newPrefs.add(this.toPreferences()); - sharedPrefs.edit().putStringSet(PREF_MIBAND_ALARMS, newPrefs).apply(); + prefs.getPreferences().edit().putStringSet(PREF_MIBAND_ALARMS, newPrefs).apply(); } public static final Creator CREATOR = new Creator() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java index 382230042..1e46d9782 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java @@ -10,10 +10,16 @@ import android.support.v4.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; +import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails; public class GBDevice implements Parcelable { public static final String ACTION_DEVICE_CHANGED @@ -34,6 +40,8 @@ public class GBDevice implements Parcelable { public static final short BATTERY_UNKNOWN = -1; private static final short BATTERY_THRESHOLD_PERCENT = 10; public static final String EXTRA_DEVICE = "device"; + private static final String DEVINFO_HW_VER = "HW: "; + private static final String DEVINFO_FW_VER = "FW: "; private final String mName; private final String mAddress; private final DeviceType mDeviceType; @@ -45,6 +53,7 @@ public class GBDevice implements Parcelable { private BatteryState mBatteryState; private short mRssi = RSSI_UNKNOWN; private String mBusyTask; + private List mDeviceInfos; public GBDevice(String address, String name, DeviceType deviceType) { mAddress = address; @@ -65,6 +74,7 @@ public class GBDevice implements Parcelable { mBatteryState = (BatteryState) in.readSerializable(); mRssi = (short) in.readInt(); mBusyTask = in.readString(); + mDeviceInfos = in.readArrayList(getClass().getClassLoader()); validate(); } @@ -82,6 +92,7 @@ public class GBDevice implements Parcelable { dest.writeSerializable(mBatteryState); dest.writeInt(mRssi); dest.writeString(mBusyTask); + dest.writeList(mDeviceInfos); } private void validate() { @@ -192,35 +203,29 @@ public class GBDevice implements Parcelable { } public String getStateString() { + /* + * for simplicity the user wont see all internal states, just connecting -> connected + * instead of connecting->connected->initializing->initialized + */ switch (mState) { case NOT_CONNECTED: return GBApplication.getContext().getString(R.string.not_connected); case WAITING_FOR_RECONNECT: return GBApplication.getContext().getString(R.string.waiting_for_reconnect); case CONNECTING: - return GBApplication.getContext().getString(R.string.connecting); case CONNECTED: - return GBApplication.getContext().getString(R.string.connected); case INITIALIZING: - return GBApplication.getContext().getString(R.string.initializing); + return GBApplication.getContext().getString(R.string.connecting); + case AUTHENTICATION_REQUIRED: + return GBApplication.getContext().getString(R.string.authentication_required); + case AUTHENTICATING: + return GBApplication.getContext().getString(R.string.authenticating); case INITIALIZED: - return GBApplication.getContext().getString(R.string.initialized); + return GBApplication.getContext().getString(R.string.connected); } return GBApplication.getContext().getString(R.string.unknown_state); } - - public String getInfoString() { - if (mFirmwareVersion != null) { - if (mHardwareVersion != null) { - return GBApplication.getContext().getString(R.string.connectionstate_hw_fw, mHardwareVersion, mFirmwareVersion); - } - return GBApplication.getContext().getString(R.string.connectionstate_fw, mFirmwareVersion); - } else { - return ""; - } - } - public DeviceType getType() { return mDeviceType; } @@ -326,6 +331,49 @@ public class GBDevice implements Parcelable { return ""; } + public boolean hasDeviceInfos() { + return getDeviceInfos().size() > 0; + } + + public List getDeviceInfos() { + List result = new ArrayList<>(); + if (mDeviceInfos != null) { + result.addAll(mDeviceInfos); + } + if (mHardwareVersion != null) { + result.add(new GenericItem(DEVINFO_HW_VER, mHardwareVersion)); + } + if (mFirmwareVersion != null) { + result.add(new GenericItem(DEVINFO_FW_VER, mFirmwareVersion)); + } + Collections.sort(result); + return result; + } + + public void setDeviceInfos(List deviceInfos) { + this.mDeviceInfos = deviceInfos; + } + + public void addDeviceInfo(ItemWithDetails info) { + if (mDeviceInfos == null) { + mDeviceInfos = new ArrayList<>(); + } else { + int index = mDeviceInfos.indexOf(info); + if (index >= 0) { + mDeviceInfos.set(index, info); // replace item with new one + return; + } + } + mDeviceInfos.add(info); + } + + public boolean removeDeviceInfo(ItemWithDetails info) { + if (mDeviceInfos == null) { + return false; + } + return mDeviceInfos.remove(info); + } + public enum State { // Note: the order is important! NOT_CONNECTED, @@ -333,6 +381,8 @@ public class GBDevice implements Parcelable { CONNECTING, CONNECTED, INITIALIZING, + AUTHENTICATION_REQUIRED, // some kind of pairing is required by the device + AUTHENTICATING, // some kind of pairing is requested by the device /** * Means that the device is connected AND all the necessary initialization steps * have been performed. At the very least, this means that basic information like diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java index 9743a0832..d2280d177 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java @@ -12,6 +12,7 @@ public class GBDeviceApp { private final UUID uuid; private final Type type; private final boolean inCache; + private final boolean configurable; public GBDeviceApp(UUID uuid, String name, String creator, String version, Type type) { this.uuid = uuid; @@ -21,9 +22,10 @@ public class GBDeviceApp { this.type = type; //FIXME: do not assume this.inCache = false; + this.configurable = false; } - public GBDeviceApp(JSONObject json) { + public GBDeviceApp(JSONObject json, boolean configurable) { UUID uuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); String name = ""; String creator = ""; @@ -47,6 +49,7 @@ public class GBDeviceApp { this.type = type; //FIXME: do not assume this.inCache = true; + this.configurable = configurable; } public boolean isInCache() { @@ -94,4 +97,8 @@ public class GBDeviceApp { } return json; } + + public boolean isConfigurable() { + return configurable; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java index cee7a5e2e..61af3fcb8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -10,9 +10,10 @@ import java.util.ArrayList; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; -import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; public class GBDeviceService implements DeviceService { @@ -115,19 +116,23 @@ public class GBDeviceService implements DeviceService { } @Override - public void onSetCallState(String number, String name, ServiceCommand command) { + public void onSetCallState(CallSpec callSpec) { // name is actually ignored and provided by the service itself... Intent intent = createIntent().setAction(ACTION_CALLSTATE) - .putExtra(EXTRA_CALL_PHONENUMBER, number) - .putExtra(EXTRA_CALL_COMMAND, command); + .putExtra(EXTRA_CALL_PHONENUMBER, callSpec.number) + .putExtra(EXTRA_CALL_COMMAND, callSpec.command); invokeService(intent); } @Override - public void onSetMusicInfo(String artist, String album, String track) { + public void onSetMusicInfo(MusicSpec musicSpec) { Intent intent = createIntent().setAction(ACTION_SETMUSICINFO) - .putExtra(EXTRA_MUSIC_ARTIST, artist) - .putExtra(EXTRA_MUSIC_TRACK, track); + .putExtra(EXTRA_MUSIC_ARTIST, musicSpec.artist) + .putExtra(EXTRA_MUSIC_ALBUM, musicSpec.album) + .putExtra(EXTRA_MUSIC_TRACK, musicSpec.track) + .putExtra(EXTRA_MUSIC_DURATION, musicSpec.duration) + .putExtra(EXTRA_MUSIC_TRACKCOUNT, musicSpec.trackCount) + .putExtra(EXTRA_MUSIC_TRACKNR, musicSpec.trackNr); invokeService(intent); } @@ -159,6 +164,14 @@ public class GBDeviceService implements DeviceService { invokeService(intent); } + @Override + public void onAppConfiguration(UUID uuid, String config) { + Intent intent = createIntent().setAction(ACTION_APP_CONFIGURE) + .putExtra(EXTRA_APP_UUID, uuid) + .putExtra(EXTRA_APP_CONFIG, config); + invokeService(intent); + } + @Override public void onFetchActivityData() { Intent intent = createIntent().setAction(ACTION_FETCH_ACTIVITY_DATA); @@ -171,6 +184,12 @@ public class GBDeviceService implements DeviceService { invokeService(intent); } + @Override + public void onHeartRateTest() { + Intent intent = createIntent().setAction(ACTION_HEARTRATE_TEST); + invokeService(intent); + } + @Override public void onFindDevice(boolean start) { Intent intent = createIntent().setAction(ACTION_FIND_DEVICE) @@ -187,7 +206,21 @@ public class GBDeviceService implements DeviceService { @Override public void onEnableRealtimeSteps(boolean enable) { Intent intent = createIntent().setAction(ACTION_ENABLE_REALTIME_STEPS) - .putExtra(EXTRA_ENABLE_REALTIME_STEPS, enable); + .putExtra(EXTRA_BOOLEAN_ENABLE, enable); + invokeService(intent); + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + Intent intent = createIntent().setAction(ACTION_ENABLE_HEARTRATE_SLEEP_SUPPORT) + .putExtra(EXTRA_BOOLEAN_ENABLE, enable); + invokeService(intent); + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + Intent intent = createIntent().setAction(ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT) + .putExtra(EXTRA_BOOLEAN_ENABLE, enable); invokeService(intent); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java index a857c9d05..df7bbce39 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java @@ -14,8 +14,8 @@ public class ActivityKind { public static final int TYPE_SLEEP = TYPE_LIGHT_SLEEP | TYPE_DEEP_SLEEP; public static final int TYPE_ALL = TYPE_ACTIVITY | TYPE_SLEEP | TYPE_NOT_WORN; - public static byte[] mapToDBActivityTypes(int types, SampleProvider provider) { - byte[] result = new byte[3]; + public static int[] mapToDBActivityTypes(int types, SampleProvider provider) { + int[] result = new int[3]; int i = 0; if ((types & ActivityKind.TYPE_ACTIVITY) != 0) { result[i++] = provider.toRawActivityKind(TYPE_ACTIVITY); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java index 99a2d28e4..117a8a7a5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java @@ -18,7 +18,7 @@ public interface ActivitySample { /** * Returns the raw activity kind value as recorded by the SampleProvider */ - byte getRawKind(); + int getRawKind(); /** * Returns the activity kind value as recorded by the SampleProvider @@ -30,7 +30,7 @@ public interface ActivitySample { /** * Returns the raw intensity value as recorded by the SampleProvider */ - short getRawIntensity(); + int getRawIntensity(); /** * Returns the normalized intensity value between 0 and 1 @@ -40,5 +40,7 @@ public interface ActivitySample { /** * Returns the number of steps performed during the period of this sample */ - short getSteps(); + int getSteps(); + + int getCustomValue(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java new file mode 100644 index 000000000..358c4a8ea --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java @@ -0,0 +1,94 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +import java.util.Calendar; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +/** + * Class holding the common user information needed by most activity trackers + */ +public class ActivityUser { + + private Integer activityUserGender; + private Integer activityUserYearOfBirth; + private Integer activityUserHeightCm; + private Integer activityUserWeightKg; + private Integer activityUserSleepDuration; + + public static final int defaultUserGender = 0; + public static final int defaultUserYearOfBirth = 0; + public static final int defaultUserAge = 0; + public static final int defaultUserHeightCm = 175; + public static final int defaultUserWeightKg = 70; + public static final int defaultUserSleepDuration = 7; + + public static final String PREF_USER_YEAR_OF_BIRTH = "activity_user_year_of_birth"; + public static final String PREF_USER_GENDER = "activity_user_gender"; + public static final String PREF_USER_HEIGHT_CM = "activity_user_height_cm"; + public static final String PREF_USER_WEIGHT_KG = "activity_user_weight_kg"; + public static final String PREF_USER_SLEEP_DURATION = "activity_user_sleep_duration"; + + public int getActivityUserWeightKg() { + if (activityUserWeightKg == null) { + fetchPreferences(); + } + return activityUserWeightKg; + } + + public int getActivityUserGender() { + if (activityUserGender == null) { + fetchPreferences(); + } + return activityUserGender; + } + + public int getActivityUserYearOfBirth() { + if (activityUserYearOfBirth == null) { + fetchPreferences(); + } + return activityUserYearOfBirth; + } + + public int getActivityUserHeightCm() { + if (activityUserHeightCm == null) { + fetchPreferences(); + } + return activityUserHeightCm; + } + + /** + * @return the user defined sleep duration or the default value when none is set or the stored + * value is out of any logical bounds. + */ + public int getActivityUserSleepDuration() { + if (activityUserSleepDuration == null) { + fetchPreferences(); + } + if (activityUserSleepDuration < 1 || activityUserSleepDuration > 24) { + activityUserSleepDuration = defaultUserSleepDuration; + } + return activityUserSleepDuration; + } + + public int getActivityUserAge() { + int userYear = getActivityUserYearOfBirth(); + int age = 25; + if (userYear > 1900) { + age = Calendar.getInstance().get(Calendar.YEAR) - userYear; + if (age <= 0) { + age = 25; + } + } + return age; + } + + private void fetchPreferences() { + Prefs prefs = GBApplication.getPrefs(); + activityUserGender = prefs.getInt(PREF_USER_GENDER, defaultUserGender); + activityUserHeightCm = prefs.getInt(PREF_USER_HEIGHT_CM, defaultUserHeightCm); + activityUserWeightKg = prefs.getInt(PREF_USER_WEIGHT_KG, defaultUserWeightKg); + activityUserYearOfBirth = prefs.getInt(PREF_USER_YEAR_OF_BIRTH, defaultUserYearOfBirth); + activityUserSleepDuration = prefs.getInt(PREF_USER_SLEEP_DURATION, defaultUserSleepDuration); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CalendarEvents.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CalendarEvents.java index 61c5b7bac..296ebda0d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CalendarEvents.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CalendarEvents.java @@ -22,7 +22,7 @@ public class CalendarEvents { // needed for miband: // time - private static final String[] EVENT_INSTANCE_PROJECTION = new String[] { + private static final String[] EVENT_INSTANCE_PROJECTION = new String[]{ Instances._ID, Instances.BEGIN, Instances.END, @@ -54,11 +54,11 @@ public class CalendarEvents { ContentUris.appendId(eventsUriBuilder, dtEnd); Uri eventsUri = eventsUriBuilder.build(); - Cursor evtCursor = null; - evtCursor = mContext.getContentResolver().query(eventsUri, EVENT_INSTANCE_PROJECTION, null, null, CalendarContract.Instances.BEGIN + " ASC"); - - if (evtCursor.moveToFirst()) { - do { + try (Cursor evtCursor = mContext.getContentResolver().query(eventsUri, EVENT_INSTANCE_PROJECTION, null, null, CalendarContract.Instances.BEGIN + " ASC")) { + if (evtCursor == null || evtCursor.getCount() == 0) { + return false; + } + while (evtCursor.moveToNext()) { CalendarEvent calEvent = new CalendarEvent( evtCursor.getLong(1), evtCursor.getLong(2), @@ -67,13 +67,11 @@ public class CalendarEvents { evtCursor.getString(5), evtCursor.getString(6), evtCursor.getString(7) - ); + ); calendarEventList.add(calEvent); - } while(evtCursor.moveToNext()); - + } return true; } - return false; } public class CalendarEvent { @@ -100,7 +98,7 @@ public class CalendarEvents { } public int getBeginSeconds() { - return (int)(begin/1000); + return (int) (begin / 1000); } public long getEnd() { @@ -112,11 +110,11 @@ public class CalendarEvents { } public int getDurationSeconds() { - return (int)((getDuration())/1000); + return (int) ((getDuration()) / 1000); } public short getDurationMinutes() { - return (short)(getDurationSeconds()/60); + return (short) (getDurationSeconds() / 60); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CallSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CallSpec.java new file mode 100644 index 000000000..1d088a493 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CallSpec.java @@ -0,0 +1,15 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +public class CallSpec { + public static final int CALL_UNDEFINED = 1; + public static final int CALL_ACCEPT = 1; + public static final int CALL_INCOMING = 2; + public static final int CALL_OUTGOING = 3; + public static final int CALL_REJECT = 4; + public static final int CALL_START = 5; + public static final int CALL_END = 6; + + public String number; + public String name; + public int command; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index af4a6d1b8..2cf979cda 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -15,7 +15,6 @@ public interface DeviceService extends EventHandler { String ACTION_START = PREFIX + ".action.start"; String ACTION_CONNECT = PREFIX + ".action.connect"; String ACTION_NOTIFICATION = PREFIX + ".action.notification"; - String ACTION_NOTIFICATION_SMS = PREFIX + ".action.notification_sms"; String ACTION_CALLSTATE = PREFIX + ".action.callstate"; String ACTION_SETTIME = PREFIX + ".action.settime"; String ACTION_SETMUSICINFO = PREFIX + ".action.setmusicinfo"; @@ -24,15 +23,19 @@ public interface DeviceService extends EventHandler { String ACTION_REQUEST_SCREENSHOT = PREFIX + ".action.request_screenshot"; String ACTION_STARTAPP = PREFIX + ".action.startapp"; String ACTION_DELETEAPP = PREFIX + ".action.deleteapp"; + String ACTION_APP_CONFIGURE = PREFIX + ".action.app_configure"; String ACTION_INSTALL = PREFIX + ".action.install"; String ACTION_REBOOT = PREFIX + ".action.reboot"; + String ACTION_HEARTRATE_TEST = PREFIX + ".action.heartrate_test"; String ACTION_FETCH_ACTIVITY_DATA = PREFIX + ".action.fetch_activity_data"; String ACTION_DISCONNECT = PREFIX + ".action.disconnect"; String ACTION_FIND_DEVICE = PREFIX + ".action.find_device"; String ACTION_SET_ALARMS = PREFIX + ".action.set_alarms"; String ACTION_ENABLE_REALTIME_STEPS = PREFIX + ".action.enable_realtime_steps"; String ACTION_REALTIME_STEPS = PREFIX + ".action.realtime_steps"; - + String ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT = PREFIX + ".action.realtime_hr_measurement"; + String ACTION_ENABLE_HEARTRATE_SLEEP_SUPPORT = PREFIX + ".action.enable_heartrate_sleep_support"; + String ACTION_HEARTRATE_MEASUREMENT = PREFIX + ".action.hr_measurement"; String EXTRA_DEVICE_ADDRESS = "device_address"; String EXTRA_NOTIFICATION_BODY = "notification_body"; String EXTRA_NOTIFICATION_FLAGS = "notification_flags"; @@ -49,14 +52,19 @@ public interface DeviceService extends EventHandler { String EXTRA_MUSIC_ARTIST = "music_artist"; String EXTRA_MUSIC_ALBUM = "music_album"; String EXTRA_MUSIC_TRACK = "music_track"; + String EXTRA_MUSIC_DURATION = "music_duration"; + String EXTRA_MUSIC_TRACKNR = "music_tracknr"; + String EXTRA_MUSIC_TRACKCOUNT = "music_trackcount"; String EXTRA_APP_UUID = "app_uuid"; String EXTRA_APP_START = "app_start"; + String EXTRA_APP_CONFIG = "app_config"; String EXTRA_URI = "uri"; String EXTRA_ALARMS = "alarms"; String EXTRA_PERFORM_PAIR = "perform_pair"; - String EXTRA_ENABLE_REALTIME_STEPS = "enable_realtime_steps"; + String EXTRA_BOOLEAN_ENABLE = "enable_realtime_steps"; String EXTRA_REALTIME_STEPS = "realtime_steps"; String EXTRA_TIMESTAMP = "timestamp"; + String EXTRA_HEART_RATE_VALUE = "hr_value"; void start(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java index f2350768b..1e28c8694 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java @@ -1,10 +1,31 @@ package nodomain.freeyourgadget.gadgetbridge.model; +import android.os.Parcel; +import android.os.Parcelable; + +import java.text.Collator; + public class GenericItem implements ItemWithDetails { private String name; private String details; private int icon; + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public GenericItem createFromParcel(Parcel source) { + GenericItem item = new GenericItem(); + item.setName(source.readString()); + item.setDetails(source.readString()); + item.setIcon(source.readInt()); + return item; + } + + @Override + public GenericItem[] newArray(int size) { + return new GenericItem[size]; + } + }; + public GenericItem(String name, String details) { this.name = name; this.details = details; @@ -17,6 +38,13 @@ public class GenericItem implements ItemWithDetails { public GenericItem() { } + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getName()); + dest.writeString(getDetails()); + dest.writeInt(getIcon()); + } + public void setName(String name) { this.name = name; } @@ -43,4 +71,38 @@ public class GenericItem implements ItemWithDetails { public int getIcon() { return icon; } + + @Override + public int describeContents() { + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GenericItem that = (GenericItem) o; + + return !(getName() != null ? !getName().equals(that.getName()) : that.getName() != null); + + } + + @Override + public int hashCode() { + return getName() != null ? getName().hashCode() : 0; + } + + @Override + public int compareTo(ItemWithDetails another) { + if (getName() == another.getName()) { + return 0; + } + if (getName() == null) { + return +1; + } else if (another.getName() == null) { + return -1; + } + return Collator.getInstance().compare(getName(), another.getName()); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ItemWithDetails.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ItemWithDetails.java index dcb768831..078ced99b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ItemWithDetails.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ItemWithDetails.java @@ -1,9 +1,18 @@ package nodomain.freeyourgadget.gadgetbridge.model; -public interface ItemWithDetails { +import android.os.Parcelable; + +public interface ItemWithDetails extends Parcelable, Comparable { String getName(); String getDetails(); int getIcon(); + + /** + * Equality is based on #getName() only. + * + * @param other + */ + boolean equals(Object other); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Measurement.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Measurement.java new file mode 100644 index 000000000..f841da00e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Measurement.java @@ -0,0 +1,33 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +public class Measurement { + private final int value; + private final long timestamp; + + public Measurement(int value, long timestamp) { + this.value = value; + this.timestamp = timestamp; + } + + public int getValue() { + return value; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public int hashCode() { + return (int) (71 ^ value ^ timestamp); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Measurement) { + Measurement m = (Measurement) o; + return timestamp == m.timestamp && value == m.value; + } + return super.equals(o); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java new file mode 100644 index 000000000..af52f02f1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java @@ -0,0 +1,17 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +public class MusicSpec { + public static final int MUSIC_UNDEFINED = 0; + public static final int MUSIC_PLAY = 1; + public static final int MUSIC_PAUSE = 2; + public static final int MUSIC_PLAYPAUSE = 3; + public static final int MUSIC_NEXT = 4; + public static final int MUSIC_PREVIOUS = 5; + + public String artist; + public String album; + public String track; + public int duration; + public int trackCount; + public int trackNr; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ServiceCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ServiceCommand.java deleted file mode 100644 index 5cb945119..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ServiceCommand.java +++ /dev/null @@ -1,22 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.model; - -public enum ServiceCommand { - - UNDEFINED, - - CALL_ACCEPT, - CALL_END, - CALL_INCOMING, - CALL_OUTGOING, - CALL_REJECT, - CALL_START, - - MUSIC_PLAY, - MUSIC_PAUSE, - MUSIC_PLAYPAUSE, - MUSIC_NEXT, - MUSIC_PREVIOUS, - - APP_INFO_NAME, - VERSION_FIRMWARE -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java index ad88c1797..21d7965b9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -6,11 +6,9 @@ import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; -import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.telephony.SmsManager; @@ -32,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot; @@ -43,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.service.receivers.GBCallControlReceiver; import nodomain.freeyourgadget.gadgetbridge.service.receivers.GBMusicControlReceiver; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; // TODO: support option for a single reminder notification when notifications could not be delivered? // conditions: app was running and received notifications, but device was not connected. @@ -59,6 +59,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { protected GBDevice gbDevice; private BluetoothAdapter btAdapter; private Context context; + private boolean autoReconnect; public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) { this.gbDevice = gbDevice; @@ -81,6 +82,16 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { return gbDevice.isInitialized(); } + @Override + public void setAutoReconnect(boolean enable) { + autoReconnect = enable; + } + + @Override + public boolean getAutoReconnect() { + return autoReconnect; + } + @Override public GBDevice getDevice() { return gbDevice; @@ -246,8 +257,8 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { Intent notificationListenerIntent = new Intent(action); notificationListenerIntent.putExtra("handle", deviceEvent.handle); if (deviceEvent.reply != null) { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(GBApplication.getContext()); - String suffix = sharedPrefs.getString("canned_reply_suffix", null); + Prefs prefs = GBApplication.getPrefs(); + String suffix = prefs.getString("canned_reply_suffix", null); if (suffix != null && !Objects.equals(suffix, "")) { deviceEvent.reply += suffix; } @@ -280,4 +291,14 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { gbDevice.sendDeviceUpdateIntent(context); } + public void handleGBDeviceEvent(GBDeviceEventDisplayMessage message) { + GB.log(message.message, message.severity, null); + + Intent messageIntent = new Intent(GB.ACTION_DISPLAY_MESSAGE); + messageIntent.putExtra(GB.DISPLAY_MESSAGE_MESSAGE, message.message); + messageIntent.putExtra(GB.DISPLAY_MESSAGE_DURATION, message.duration); + messageIntent.putExtra(GB.DISPLAY_MESSAGE_SEVERITY, message.severity); + + LocalBroadcastManager.getInstance(context).sendBroadcast(messageIntent); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index d85c12909..7738f40c7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -3,7 +3,6 @@ package nodomain.freeyourgadget.gadgetbridge.service; import android.app.NotificationManager; import android.app.Service; import android.content.BroadcastReceiver; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -11,7 +10,6 @@ import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.IBinder; -import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.support.annotation.Nullable; import android.support.v4.content.LocalBroadcastManager; @@ -34,19 +32,26 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.SMSReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; -import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_APP_CONFIGURE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CALLSTATE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETEAPP; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DISCONNECT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_HEARTRATE_SLEEP_SUPPORT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ENABLE_REALTIME_STEPS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_FETCH_ACTIVITY_DATA; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_FIND_DEVICE; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_HEARTRATE_TEST; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_INSTALL; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_NOTIFICATION; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REBOOT; @@ -59,16 +64,20 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SE import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_ALARMS; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_CONFIG; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_UUID; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_BOOLEAN_ENABLE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_COMMAND; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_PHONENUMBER; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_DEVICE_ADDRESS; -import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_ENABLE_REALTIME_STEPS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_DURATION; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACK; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACKCOUNT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACKNR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_BODY; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_FLAGS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ID; @@ -81,7 +90,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOT import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_PERFORM_PAIR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_URI; -public class DeviceCommunicationService extends Service { +public class DeviceCommunicationService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener { private static final Logger LOG = LoggerFactory.getLogger(DeviceCommunicationService.class); private boolean mStarted = false; @@ -109,7 +118,7 @@ public class DeviceCommunicationService extends Service { mGBDevice = device; boolean enableReceivers = mDeviceSupport != null && (mDeviceSupport.useAutoConnect() || mGBDevice.isInitialized()); setReceiversEnableState(enableReceivers); - GB.updateNotification(mGBDevice.getName() + " " + mGBDevice.getStateString(), context); + GB.updateNotification(mGBDevice.getName() + " " + mGBDevice.getStateString(), mGBDevice.isInitialized(), context); } else { LOG.error("Got ACTION_DEVICE_CHANGED from unexpected device: " + mGBDevice); } @@ -123,6 +132,10 @@ public class DeviceCommunicationService extends Service { super.onCreate(); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED)); mFactory = new DeviceSupportFactory(this); + + if (hasPrefs()) { + getPrefs().getPreferences().registerOnSharedPreferenceChangeListener(this); + } } @Override @@ -162,6 +175,7 @@ public class DeviceCommunicationService extends Service { // when we get past this, we should have valid mDeviceSupport and mGBDevice instances + Prefs prefs = getPrefs(); switch (action) { case ACTION_START: start(); @@ -169,19 +183,23 @@ public class DeviceCommunicationService extends Service { case ACTION_CONNECT: start(); // ensure started GBDevice gbDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + String btDeviceAddress = null; if (gbDevice == null) { - String btDeviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS); - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - if (sharedPrefs != null) { // may be null in test cases - if (btDeviceAddress == null) { - btDeviceAddress = sharedPrefs.getString("last_device_address", null); - } else { - sharedPrefs.edit().putString("last_device_address", btDeviceAddress).apply(); - } + btDeviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS); + if (btDeviceAddress == null && prefs != null) { // may be null in test cases + btDeviceAddress = prefs.getString("last_device_address", null); } if (btDeviceAddress != null) { gbDevice = DeviceHelper.getInstance().findAvailableDevice(btDeviceAddress, this); } + } else { + btDeviceAddress = gbDevice.getAddress(); + } + + boolean autoReconnect = GBPrefs.AUTO_RECONNECT_DEFAULT; + if (prefs != null && prefs.getPreferences() != null) { + prefs.getPreferences().edit().putString("last_device_address", btDeviceAddress).apply(); + autoReconnect = getGBPrefs().getAutoReconnect(); } if (gbDevice != null && !isConnecting() && !isConnected()) { @@ -193,6 +211,7 @@ public class DeviceCommunicationService extends Service { if (pair) { deviceSupport.pair(); } else { + deviceSupport.setAutoReconnect(autoReconnect); deviceSupport.connect(); } } else { @@ -230,13 +249,12 @@ public class DeviceCommunicationService extends Service { if (((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_REPLY) > 0) || (notificationSpec.type == NotificationType.SMS && notificationSpec.phoneNumber != null)) { // NOTE: maybe not where it belongs - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - if (sharedPrefs.getBoolean("pebble_force_untested", false)) { + if (prefs.getBoolean("pebble_force_untested", false)) { // I would rather like to save that as an array in ShadredPreferences // this would work but I dont know how to do the same in the Settings Activity's xml ArrayList replies = new ArrayList<>(); for (int i = 1; i <= 16; i++) { - String reply = sharedPrefs.getString("canned_reply_" + i, null); + String reply = prefs.getString("canned_reply_" + i, null); if (reply != null && !reply.equals("")) { replies.add(reply); } @@ -251,6 +269,10 @@ public class DeviceCommunicationService extends Service { mDeviceSupport.onReboot(); break; } + case ACTION_HEARTRATE_TEST: { + mDeviceSupport.onHeartRateTest(); + break; + } case ACTION_FETCH_ACTIVITY_DATA: { mDeviceSupport.onFetchActivityData(); break; @@ -266,23 +288,32 @@ public class DeviceCommunicationService extends Service { break; } case ACTION_CALLSTATE: - ServiceCommand command = (ServiceCommand) intent.getSerializableExtra(EXTRA_CALL_COMMAND); + int command = intent.getIntExtra(EXTRA_CALL_COMMAND, CallSpec.CALL_UNDEFINED); String phoneNumber = intent.getStringExtra(EXTRA_CALL_PHONENUMBER); String callerName = null; if (phoneNumber != null) { callerName = getContactDisplayNameByNumber(phoneNumber); } - mDeviceSupport.onSetCallState(phoneNumber, callerName, command); + + CallSpec callSpec = new CallSpec(); + callSpec.command = command; + callSpec.number = phoneNumber; + callSpec.name = callerName; + mDeviceSupport.onSetCallState(callSpec); break; case ACTION_SETTIME: mDeviceSupport.onSetTime(); break; case ACTION_SETMUSICINFO: - String artist = intent.getStringExtra(EXTRA_MUSIC_ARTIST); - String album = intent.getStringExtra(EXTRA_MUSIC_ALBUM); - String track = intent.getStringExtra(EXTRA_MUSIC_TRACK); - mDeviceSupport.onSetMusicInfo(artist, album, track); + MusicSpec musicSpec = new MusicSpec(); + musicSpec.artist = intent.getStringExtra(EXTRA_MUSIC_ARTIST); + musicSpec.album = intent.getStringExtra(EXTRA_MUSIC_ALBUM); + musicSpec.track = intent.getStringExtra(EXTRA_MUSIC_TRACK); + musicSpec.duration = intent.getIntExtra(EXTRA_MUSIC_DURATION, 0); + musicSpec.trackCount = intent.getIntExtra(EXTRA_MUSIC_TRACKCOUNT, 0); + musicSpec.trackNr = intent.getIntExtra(EXTRA_MUSIC_TRACKNR, 0); + mDeviceSupport.onSetMusicInfo(musicSpec); break; case ACTION_REQUEST_APPINFO: mDeviceSupport.onAppInfoReq(); @@ -301,6 +332,11 @@ public class DeviceCommunicationService extends Service { mDeviceSupport.onAppDelete(uuid); break; } + case ACTION_APP_CONFIGURE: { + UUID uuid = (UUID) intent.getSerializableExtra(EXTRA_APP_UUID); + String config = intent.getStringExtra(EXTRA_APP_CONFIG); + mDeviceSupport.onAppConfiguration(uuid, config); + } case ACTION_INSTALL: Uri uri = intent.getParcelableExtra(EXTRA_URI); if (uri != null) { @@ -312,10 +348,21 @@ public class DeviceCommunicationService extends Service { ArrayList alarms = intent.getParcelableArrayListExtra(EXTRA_ALARMS); mDeviceSupport.onSetAlarms(alarms); break; - case ACTION_ENABLE_REALTIME_STEPS: - boolean enable = intent.getBooleanExtra(EXTRA_ENABLE_REALTIME_STEPS, false); + case ACTION_ENABLE_REALTIME_STEPS: { + boolean enable = intent.getBooleanExtra(EXTRA_BOOLEAN_ENABLE, false); mDeviceSupport.onEnableRealtimeSteps(enable); break; + } + case ACTION_ENABLE_HEARTRATE_SLEEP_SUPPORT: { + boolean enable = intent.getBooleanExtra(EXTRA_BOOLEAN_ENABLE, false); + mDeviceSupport.onEnableHeartRateSleepSupport(enable); + break; + } + case ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT: { + boolean enable = intent.getBooleanExtra(EXTRA_BOOLEAN_ENABLE, false); + mDeviceSupport.onEnableRealtimeHeartRateMeasurement(enable); + break; + } } return START_STICKY; @@ -348,7 +395,7 @@ public class DeviceCommunicationService extends Service { private void start() { if (!mStarted) { - startForeground(GB.NOTIFICATION_ID, GB.createNotification(getString(R.string.gadgetbridge_running), this)); + startForeground(GB.NOTIFICATION_ID, GB.createNotification(getString(R.string.gadgetbridge_running), false, this)); mStarted = true; } } @@ -398,7 +445,10 @@ public class DeviceCommunicationService extends Service { } if (mMusicPlaybackReceiver == null) { mMusicPlaybackReceiver = new MusicPlaybackReceiver(); - registerReceiver(mMusicPlaybackReceiver, new IntentFilter("com.android.music.metachanged")); + IntentFilter filter = new IntentFilter(); + filter.addAction("com.android.music.metachanged"); + //filter.addAction("com.android.music.playstatechanged"); + registerReceiver(mMusicPlaybackReceiver, filter); } if (mTimeChangeReceiver == null) { mTimeChangeReceiver = new TimeChangeReceiver(); @@ -437,6 +487,10 @@ public class DeviceCommunicationService extends Service { @Override public void onDestroy() { + if (hasPrefs()) { + getPrefs().getPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + LOG.debug("DeviceCommunicationService is being destroyed"); super.onDestroy(); @@ -462,26 +516,37 @@ public class DeviceCommunicationService extends Service { return name; } - ContentResolver contentResolver = getContentResolver(); - - Cursor contactLookup = null; - try { - contactLookup = contentResolver.query(uri, null, null, null, null); - } catch (SecurityException e) { - return name; - } - - try { + try (Cursor contactLookup = getContentResolver().query(uri, null, null, null, null)) { if (contactLookup != null && contactLookup.getCount() > 0) { contactLookup.moveToNext(); name = contactLookup.getString(contactLookup.getColumnIndex(ContactsContract.Data.DISPLAY_NAME)); } - } finally { - if (contactLookup != null) { - contactLookup.close(); - } + } catch (SecurityException e) { + // ignore, just return name below } return name; } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (GBPrefs.AUTO_RECONNECT.equals(key)) { + boolean autoReconnect = getGBPrefs().getAutoReconnect(); + if (mDeviceSupport != null) { + mDeviceSupport.setAutoReconnect(autoReconnect); + } + } + } + + protected boolean hasPrefs() { + return getPrefs().getPreferences() != null; + } + + public Prefs getPrefs() { + return GBApplication.getPrefs(); + } + + public GBPrefs getGBPrefs() { + return GBApplication.getGBPrefs(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupport.java index bce4e64b7..61646c54a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupport.java @@ -62,6 +62,20 @@ public interface DeviceSupport extends EventHandler { */ boolean useAutoConnect(); + /** + * Configures this instance to automatically attempt to reconnect after a connection loss. + * How, how long, or how often is up to the implementation. + * Note that tome implementations may not support automatic reconnection at all. + * @param enable + */ + void setAutoReconnect(boolean enable); + + /** + * Returns whether this instance to configured to automatically attempt to reconnect after a + * connection loss. + */ + boolean getAutoReconnect(); + /** * Attempts to pair and connect this device with the gadget device. Success * will be reported via a device change Intent. diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index 55765588d..6bc077896 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -1,7 +1,6 @@ package nodomain.freeyourgadget.gadgetbridge.service; import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; import android.content.Context; import android.widget.Toast; @@ -11,10 +10,8 @@ import java.util.EnumSet; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; -import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class DeviceSupportFactory { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index b75b1c596..8047aad60 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -13,8 +13,9 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; -import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; /** * Wraps another device support instance and supports busy-checking and throttling of events. @@ -55,6 +56,16 @@ public class ServiceDeviceSupport implements DeviceSupport { return delegate.connect(); } + @Override + public void setAutoReconnect(boolean enable) { + delegate.setAutoReconnect(enable); + } + + @Override + public boolean getAutoReconnect() { + return delegate.getAutoReconnect(); + } + @Override public void dispose() { delegate.dispose(); @@ -131,19 +142,19 @@ public class ServiceDeviceSupport implements DeviceSupport { // No throttling for the other events @Override - public void onSetCallState(String number, String name, ServiceCommand command) { + public void onSetCallState(CallSpec callSpec) { if (checkBusy("set call state")) { return; } - delegate.onSetCallState(number, name, command); + delegate.onSetCallState(callSpec); } @Override - public void onSetMusicInfo(String artist, String album, String track) { + public void onSetMusicInfo(MusicSpec musicSpec) { if (checkBusy("set music info")) { return; } - delegate.onSetMusicInfo(artist, album, track); + delegate.onSetMusicInfo(musicSpec); } @Override @@ -178,6 +189,14 @@ public class ServiceDeviceSupport implements DeviceSupport { delegate.onAppDelete(uuid); } + @Override + public void onAppConfiguration(UUID uuid, String config) { + if (checkBusy("app configuration")) { + return; + } + delegate.onAppConfiguration(uuid, config); + } + @Override public void onFetchActivityData() { if (checkBusy("fetch activity data")) { @@ -194,6 +213,14 @@ public class ServiceDeviceSupport implements DeviceSupport { delegate.onReboot(); } + @Override + public void onHeartRateTest() { + if (checkBusy("heartrate")) { + return; + } + delegate.onHeartRateTest(); + } + @Override public void onFindDevice(boolean start) { if (checkBusy("find device")) { @@ -225,4 +252,20 @@ public class ServiceDeviceSupport implements DeviceSupport { } delegate.onEnableRealtimeSteps(enable); } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + if (checkBusy("enable heartrate sleep support: " + enable)) { + return; + } + delegate.onEnableHeartRateSleepSupport(enable); + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + if (checkBusy("enable realtime heart rate measurement: " + enable)) { + return; + } + delegate.onEnableRealtimeHeartRateMeasurement(enable); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index 051c8ce61..04179458c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -16,6 +16,7 @@ import java.util.Set; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.CheckInitializedAction; /** * Abstract base class for all devices connected through Bluetooth Low Energy (LE) aka @@ -41,10 +42,19 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im public boolean connect() { if (mQueue == null) { mQueue = new BtLEQueue(getBluetoothAdapter(), getDevice(), this, getContext()); + mQueue.setAutoReconnect(getAutoReconnect()); } return mQueue.connect(); } + @Override + public void setAutoReconnect(boolean enable) { + super.setAutoReconnect(enable); + if (mQueue != null) { + mQueue.setAutoReconnect(enable); + } + } + /** * Subclasses should populate the given builder to initialize the device (if necessary). * diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java index 3c2387c01..258199ad8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java @@ -36,6 +36,7 @@ public abstract class AbstractBTLEOperation * Performs this operation. The whole operation is asynchronous, i.e. * this method quickly returns before the actual operation is finished. * Calls #prePerform() and, if successful, #doPerform(). + * * @throws IOException */ @Override @@ -48,6 +49,7 @@ public abstract class AbstractBTLEOperation /** * Hook for subclasses to perform something before #doPerform() is invoked. + * * @throws IOException */ protected void prePerform() throws IOException { @@ -58,6 +60,7 @@ public abstract class AbstractBTLEOperation * successfully. * Note that subclasses HAVE TO call #operationFinished() when the entire * opreation is done (successful or not). + * * @throws IOException */ protected abstract void doPerform() throws IOException; @@ -65,6 +68,7 @@ public abstract class AbstractBTLEOperation /** * You MUST call this method when the operation has finished, either * successfull or unsuccessfully. + * * @throws IOException */ protected void operationFinished() throws IOException { @@ -109,6 +113,10 @@ public abstract class AbstractBTLEOperation return operationStatus == OperationStatus.RUNNING; } + public boolean isOperationFinished() { + return operationStatus == OperationStatus.FINISHED; + } + public T getSupport() { return mSupport; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java index b5f3125df..f60094356 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java @@ -34,12 +34,6 @@ public final class BtLEQueue { private final GBDevice mGbDevice; private final BluetoothAdapter mBluetoothAdapter; private BluetoothGatt mBluetoothGatt; - /** - * When an automatic reconnect was attempted after a connection breakdown (error) - */ - private long lastReconnectTime = System.currentTimeMillis(); - - private static final long MIN_MILLIS_BEFORE_RECONNECT = 1000 * 60 * 5; // 5 minutes private final BlockingQueue mTransactions = new LinkedBlockingQueue<>(); private volatile boolean mDisposed; @@ -51,6 +45,7 @@ public final class BtLEQueue { private CountDownLatch mConnectionLatch; private BluetoothGattCharacteristic mWaitCharacteristic; private final InternalGattCallback internalGattCallback; + private boolean mAutoReconnect; private Thread dispatchThread = new Thread("GadgetBridge GATT Dispatcher") { @@ -63,6 +58,7 @@ public final class BtLEQueue { Transaction transaction = mTransactions.take(); if (!isConnected()) { + LOG.debug("not connected, waiting for connection..."); // TODO: request connection and initialization from the outside and wait until finished internalGattCallback.reset(); @@ -129,6 +125,10 @@ public final class BtLEQueue { dispatchThread.start(); } + public void setAutoReconnect(boolean enable) { + mAutoReconnect = enable; + } + protected boolean isConnected() { return mGbDevice.isConnected(); } @@ -156,7 +156,9 @@ public final class BtLEQueue { LOG.info("Attempting to connect to " + mGbDevice.getName()); mBluetoothAdapter.cancelDiscovery(); BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteDevice(mGbDevice.getAddress()); +// boolean result; synchronized (mGattMonitor) { + // connectGatt with true doesn't really work ;( too often connection problems mBluetoothGatt = remoteDevice.connectGatt(mContext, false, internalGattCallback); // result = mBluetoothGatt.connect(); } @@ -168,15 +170,17 @@ public final class BtLEQueue { } private void setDeviceConnectionState(State newState) { + LOG.debug("new device connection state: " + newState); mGbDevice.setState(newState); mGbDevice.sendDeviceUpdateIntent(mContext); - if (mConnectionLatch != null) { + if (mConnectionLatch != null && newState == State.CONNECTED) { mConnectionLatch.countDown(); } } public void disconnect() { synchronized (mGattMonitor) { + LOG.debug("disconnect()"); BluetoothGatt gatt = mBluetoothGatt; if (gatt != null) { mBluetoothGatt = null; @@ -189,6 +193,7 @@ public final class BtLEQueue { } private void handleDisconnected(int status) { + LOG.debug("handleDisconnected: " + status); internalGattCallback.reset(); mTransactions.clear(); mAbortTransaction = true; @@ -216,11 +221,9 @@ public final class BtLEQueue { * @return true if a reconnection attempt was made, or false otherwise */ private boolean maybeReconnect() { - long currentTime = System.currentTimeMillis(); - if (currentTime - lastReconnectTime >= MIN_MILLIS_BEFORE_RECONNECT) { - LOG.info("Automatic reconnection attempt..."); - lastReconnectTime = currentTime; - return connect(); + if (mAutoReconnect && mBluetoothGatt != null) { + LOG.info("Enabling automatic ble reconnect..."); + return mBluetoothGatt.connect(); } return false; } @@ -271,8 +274,8 @@ public final class BtLEQueue { } private boolean checkCorrectGattInstance(BluetoothGatt gatt, String where) { - if (gatt != mBluetoothGatt) { - LOG.info("Ignoring event from wrong BluetoothGatt instance: " + where); + if (gatt != mBluetoothGatt && mBluetoothGatt != null) { + LOG.info("Ignoring event from wrong BluetoothGatt instance: " + where + "; " + gatt); return false; } return true; @@ -319,7 +322,7 @@ public final class BtLEQueue { setDeviceConnectionState(State.CONNECTED); // Attempts to discover services after successful connection. LOG.info("Attempting to start service discovery:" + - mBluetoothGatt.discoverServices()); + gatt.discoverServices()); break; case BluetoothProfile.STATE_DISCONNECTED: LOG.info("Disconnected from GATT server."); @@ -418,7 +421,7 @@ public final class BtLEQueue { for (byte b : characteristic.getValue()) { content += String.format(" 0x%1x", b); } - LOG.debug("characteristic changed: " + characteristic.getUuid() + " value: " + content ); + LOG.debug("characteristic changed: " + characteristic.getUuid() + " value: " + content); } if (!checkCorrectGattInstance(gatt, "characteristic changed")) { return; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java index ec5afed58..3089856b4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java @@ -74,7 +74,7 @@ public interface GattCallback { * @see BluetoothGattCallback#onDescriptorRead(BluetoothGatt, BluetoothGattDescriptor, int) */ void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, - int status); + int status); /** * @param gatt @@ -83,7 +83,7 @@ public interface GattCallback { * @see BluetoothGattCallback#onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int) */ void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, - int status); + int status); // // /** // * @see BluetoothGattCallback#onReliableWriteCompleted(BluetoothGatt, int) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/CheckInitializedAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/CheckInitializedAction.java similarity index 85% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/CheckInitializedAction.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/CheckInitializedAction.java index 21bc0be87..414d983fc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/CheckInitializedAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/CheckInitializedAction.java @@ -1,10 +1,9 @@ -package nodomain.freeyourgadget.gadgetbridge.service.btle; +package nodomain.freeyourgadget.gadgetbridge.service.btle.actions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction; /** * A special action that is executed at the very front of the initialization diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/ConditionalWriteAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/ConditionalWriteAction.java new file mode 100644 index 000000000..6d373fafb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/ConditionalWriteAction.java @@ -0,0 +1,30 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.actions; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; + +public abstract class ConditionalWriteAction extends WriteAction { + public ConditionalWriteAction(BluetoothGattCharacteristic characteristic) { + super(characteristic, null); + } + + @Override + protected boolean writeValue(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) { + byte[] conditionalValue = checkCondition(); + if (conditionalValue != null) { + return super.writeValue(gatt, characteristic, conditionalValue); + } + return true; + } + + /** + * Checks the condition whether the write shall happen or not. + * Returns the actual value to be written or null in case nothing shall be written. + *

+ * Note that returning null will not cause run() to return false, in other words, + * the rest of the queue will still be executed. + * + * @return the value to be written or null to not write anything + */ + protected abstract byte[] checkCondition(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/SetDeviceStateAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/SetDeviceStateAction.java index a11989280..f88f4a950 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/SetDeviceStateAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/SetDeviceStateAction.java @@ -26,4 +26,9 @@ public class SetDeviceStateAction extends PlainAction { public Context getContext() { return context; } + + @Override + public String toString() { + return super.toString() + " to " + deviceState; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/SetProgressAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/SetProgressAction.java index 8f3a826bf..7a3e2d20b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/SetProgressAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/SetProgressAction.java @@ -3,9 +3,13 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.actions; import android.bluetooth.BluetoothGatt; import android.content.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import nodomain.freeyourgadget.gadgetbridge.util.GB; public class SetProgressAction extends PlainAction { + private static final Logger LOG = LoggerFactory.getLogger(SetProgressAction.class); private final String text; private final boolean ongoing; @@ -30,6 +34,7 @@ public class SetProgressAction extends PlainAction { @Override public boolean run(BluetoothGatt gatt) { + LOG.info(toString()); GB.updateInstallNotification(this.text, this.ongoing, this.percentage, this.context); return true; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java index b4ec95489..5cecee06d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java @@ -22,16 +22,26 @@ public class WriteAction extends BtLEAction { @Override public boolean run(BluetoothGatt gatt) { - int properties = getCharacteristic().getProperties(); + 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 if ((properties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0 || ((properties & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0)) { - if (getCharacteristic().setValue(value)) { - return gatt.writeCharacteristic(getCharacteristic()); - } + return writeValue(gatt, characteristic, value); } return false; } + protected boolean writeValue(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) { + if (characteristic.setValue(value)) { + return gatt.writeCharacteristic(characteristic); + } + return false; + } + + protected final byte[] getValue() { + return value; + } + @Override public boolean expectsResult() { return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1FirmwareInfo.java new file mode 100644 index 000000000..e274c9ce4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1FirmwareInfo.java @@ -0,0 +1,107 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; + +/** + * Some helper methods for Mi1 and Mi1A firmware. + */ +public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo { + private static final Logger LOG = LoggerFactory.getLogger(AbstractMi1FirmwareInfo.class); + + private static final byte[] SINGLE_FW_HEADER = new byte[]{ + 0, + (byte) 0x98, + 0, + (byte) 0x20, + (byte) 0x89, + 4, + 0, + (byte) 0x20 + }; + private static final int SINGLE_FW_HEADER_OFFSET = 0; + + private static final int MI1_FW_BASE_OFFSET = 1056; + + protected AbstractMi1FirmwareInfo(@NonNull byte[] wholeFirmwareBytes) { + super(wholeFirmwareBytes); + } + + @Override + public boolean isSingleMiBandFirmware() { + return true; + } + + @Override + public int getFirmwareOffset() { + return 0; + } + + public int getFirmwareLength() { + return wholeFirmwareBytes.length; + } + + public int getFirmwareVersion() { + return (wholeFirmwareBytes[getOffsetFirmwareVersionMajor()] << 24) + | (wholeFirmwareBytes[getOffsetFirmwareVersionMinor()] << 16) + | (wholeFirmwareBytes[getOffsetFirmwareVersionRevision()] << 8) + | wholeFirmwareBytes[getOffsetFirmwareVersionBuild()]; + } + + private int getOffsetFirmwareVersionMajor() { + return MI1_FW_BASE_OFFSET + 3; + } + + private int getOffsetFirmwareVersionMinor() { + return MI1_FW_BASE_OFFSET + 2; + } + + private int getOffsetFirmwareVersionRevision() { + return MI1_FW_BASE_OFFSET + 1; + } + + private int getOffsetFirmwareVersionBuild() { + return MI1_FW_BASE_OFFSET; + } + + @Override + protected boolean isGenerallySupportedFirmware() { + try { + if (!isHeaderValid()) { + LOG.info("unrecognized header"); + return false; + } + int majorVersion = getFirmwareVersionMajor(); + if (majorVersion == getSupportedMajorVersion()) { + return true; + } else { + LOG.info("Only major version " + getSupportedMajorVersion() + " is supported: " + majorVersion); + } + } catch (IllegalArgumentException ex) { + LOG.warn("invalid firmware or bug: " + ex.getLocalizedMessage(), ex); + } catch (IndexOutOfBoundsException ex) { + LOG.warn("not supported firmware: " + ex.getLocalizedMessage(), ex); + } + return false; + } + + 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); + } + + @Override + public void checkValid() throws IllegalArgumentException { + super.checkValid(); + + if (wholeFirmwareBytes.length < SINGLE_FW_HEADER.length) { + throw new IllegalArgumentException("firmware too small: " + wholeFirmwareBytes.length); + } + } + + protected abstract int getSupportedMajorVersion(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java new file mode 100644 index 000000000..cbe8148c6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java @@ -0,0 +1,23 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.support.annotation.NonNull; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public abstract class AbstractMi1SFirmwareInfo extends AbstractMiFirmwareInfo { + + public AbstractMi1SFirmwareInfo(@NonNull byte[] wholeFirmwareBytes) { + super(wholeFirmwareBytes); + } + + @Override + public boolean isGenerallyCompatibleWith(GBDevice device) { + return MiBandConst.MI_1S.equals(device.getHardwareVersion()); + } + + @Override + public boolean isSingleMiBandFirmware() { + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMiFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMiFirmwareInfo.java new file mode 100644 index 000000000..a4b3daf6d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMiFirmwareInfo.java @@ -0,0 +1,125 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.support.annotation.NonNull; + +import java.util.Arrays; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public abstract class AbstractMiFirmwareInfo { + + /** + * @param wholeFirmwareBytes + * @return + * @throws IllegalArgumentException when the data is not recognized as firmware data + */ + public static + @NonNull + AbstractMiFirmwareInfo determineFirmwareInfoFor(byte[] wholeFirmwareBytes) { + AbstractMiFirmwareInfo[] candidates = getFirmwareInfoCandidatesFor(wholeFirmwareBytes); + if (candidates.length == 0) { + throw new IllegalArgumentException("Unsupported data (maybe not even a firmware?)."); + } + if (candidates.length == 1) { + return candidates[0]; + } + throw new IllegalArgumentException("don't know for which device the firmware is, matches multiple devices"); + } + + private static AbstractMiFirmwareInfo[] getFirmwareInfoCandidatesFor(byte[] wholeFirmwareBytes) { + if (MiBandSupport.MI_1A_HR_FW_UPDATE_TEST_MODE_ENABLED) { + TestMi1AFirmwareInfo info = TestMi1AFirmwareInfo.getInstance(wholeFirmwareBytes); + if (info != null) { + return new AbstractMiFirmwareInfo[]{info}; + } + } + + AbstractMiFirmwareInfo[] candidates = new AbstractMiFirmwareInfo[3]; + int i = 0; + Mi1FirmwareInfo mi1Info = Mi1FirmwareInfo.getInstance(wholeFirmwareBytes); + if (mi1Info != null) { + candidates[i++] = mi1Info; + } + Mi1AFirmwareInfo mi1aInfo = Mi1AFirmwareInfo.getInstance(wholeFirmwareBytes); + if (mi1aInfo != null) { + candidates[i++] = mi1aInfo; + } + Mi1SFirmwareInfo mi1sInfo = Mi1SFirmwareInfo.getInstance(wholeFirmwareBytes); + if (mi1sInfo != null) { + candidates[i++] = mi1sInfo; + } + return Arrays.copyOfRange(candidates, 0, i); + } + + @NonNull + protected byte[] wholeFirmwareBytes; + + public AbstractMiFirmwareInfo(@NonNull byte[] wholeFirmwareBytes) { + this.wholeFirmwareBytes = wholeFirmwareBytes; + } + + public abstract int getFirmwareOffset(); + + public abstract int getFirmwareLength(); + + public abstract int getFirmwareVersion(); + + /** + * Returns true if the firmware data is recognized as such and can be + * handled by this instance. No further sanity checks are done at this point. + */ + protected abstract boolean isGenerallySupportedFirmware(); + + /** + * This method checks whether the firmware data is recognized as such and can be handled + * by this instance. It will be called by #isGenerallySupportedFirmware() in order to check + * whether this instance can be used at all or shall be thrown away. + */ + protected abstract boolean isHeaderValid(); + + /** + * Checks whether this instance, with the provided firmware data is compatible with the + * given device. Must be called to avoid installing Mi1 firmware on Mi1A, for example. + * + * @param device + */ + public abstract boolean isGenerallyCompatibleWith(GBDevice device); + + @NonNull + public byte[] getFirmwareBytes() { + return Arrays.copyOfRange(wholeFirmwareBytes, getFirmwareOffset(), getFirmwareOffset() + getFirmwareLength()); + } + + public int getFirmwareVersionMajor() { + int version = getFirmwareVersion(); + if (version > 0) { + return (version >> 24); + } + throw new IllegalArgumentException("bad firmware version: " + version); + } + + public abstract boolean isSingleMiBandFirmware(); + + /** + * Performs a thorough sanity check of the firmware data and throws IllegalArgumentException + * if there's any problem with it. + * + * @throws IllegalArgumentException + */ + public void checkValid() throws IllegalArgumentException { + } + + public AbstractMiFirmwareInfo getFirst() { + if (isSingleMiBandFirmware()) { + return this; + } + throw new UnsupportedOperationException(getClass().getName() + " must override getFirst() and getSecond()"); + } + + public AbstractMiFirmwareInfo getSecond() { + if (isSingleMiBandFirmware()) { + throw new UnsupportedOperationException(getClass().getName() + " only supports on firmware"); + } + throw new UnsupportedOperationException(getClass().getName() + " must override getFirst() and getSecond()"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/CheckAuthenticationNeededAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/CheckAuthenticationNeededAction.java new file mode 100644 index 000000000..8415c6958 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/CheckAuthenticationNeededAction.java @@ -0,0 +1,25 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction; + +public class CheckAuthenticationNeededAction extends AbortTransactionAction { + private final GBDevice mDevice; + + public CheckAuthenticationNeededAction(GBDevice device) { + super(); + mDevice = device; + } + + @Override + protected boolean shouldAbort() { + // the state is set in MiBandSupport.handleNotificationNotif() + switch (mDevice.getState()) { + case AUTHENTICATION_REQUIRED: // fall through + case AUTHENTICATING: + return true; // abort the whole thing + default: + return false; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/CompositeMiFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/CompositeMiFirmwareInfo.java new file mode 100644 index 000000000..1ec25c292 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/CompositeMiFirmwareInfo.java @@ -0,0 +1,96 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract Mi firmware info class with two child info instances. + */ +public abstract class CompositeMiFirmwareInfo extends AbstractMiFirmwareInfo { + private static final Logger LOG = LoggerFactory.getLogger(CompositeMiFirmwareInfo.class); + + private final T fw1Info; + private final T fw2Info; + + protected CompositeMiFirmwareInfo(byte[] wholeFirmwareBytes, T info1, T info2) { + super(wholeFirmwareBytes); + fw1Info = info1; + fw2Info = info2; + } + + @Override + public void checkValid() throws IllegalArgumentException { + super.checkValid(); + + if (getFirst().getFirmwareOffset() == getSecond().getFirmwareOffset()) { + throw new IllegalArgumentException("Illegal firmware offsets: " + getLengthsOffsetsString()); + } + if (getFirst().getFirmwareOffset() < 0 || getSecond().getFirmwareOffset() < 0 + || getFirst().getFirmwareLength() <= 0 || getSecond().getFirmwareLength() <= 0) { + throw new IllegalArgumentException("Illegal firmware offsets/lengths: " + getLengthsOffsetsString()); + } + + int firstEndIndex = getFirst().getFirmwareOffset() + getFirst().getFirmwareLength(); + if (getSecond().getFirmwareOffset() < firstEndIndex) { + throw new IllegalArgumentException("Invalid firmware, second fw starts before first fw ends: " + firstEndIndex + "," + getSecond().getFirmwareOffset()); + } + int secondEndIndex = getSecond().getFirmwareOffset(); + if (wholeFirmwareBytes.length < firstEndIndex || wholeFirmwareBytes.length < secondEndIndex) { + throw new IllegalArgumentException("Invalid firmware size, or invalid offsets/lengths: " + getLengthsOffsetsString()); + } + + getFirst().checkValid(); + getSecond().checkValid(); + } + + protected String getLengthsOffsetsString() { + return getFirst().getFirmwareOffset() + "," + getFirst().getFirmwareLength() + + "; " + + getSecond().getFirmwareOffset() + "," + getSecond().getFirmwareLength(); + } + + @Override + public T getFirst() { + return fw1Info; + } + + @Override + public T getSecond() { + return fw2Info; + } + + @Override + protected boolean isGenerallySupportedFirmware() { + try { + if (!isHeaderValid()) { + LOG.info("unrecognized header"); + return false; + } + return fw1Info.isGenerallySupportedFirmware() + && fw2Info.isGenerallySupportedFirmware() + && fw1Info.getFirmwareBytes().length > 0 + && fw2Info.getFirmwareBytes().length > 0; + } catch (IndexOutOfBoundsException ex) { + LOG.warn("invalid firmware or bug: " + ex.getLocalizedMessage(), ex); + return false; + } catch (IllegalArgumentException ex) { + LOG.warn("not supported 1S firmware: " + ex.getLocalizedMessage(), ex); + return false; + } + } + + @Override + public int getFirmwareOffset() { + throw new UnsupportedOperationException("call this method on getFirmwareXInfo()"); + } + + @Override + public int getFirmwareLength() { + throw new UnsupportedOperationException("call this method on getFirmwareXInfo()"); + } + + @Override + public int getFirmwareVersion() { + throw new UnsupportedOperationException("call this method on getFirmwareXInfo()"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java index 18ffe2f98..3ba149df6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java @@ -1,17 +1,23 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; public class DeviceInfo extends AbstractInfo { public final String deviceId; public final int profileVersion; + /** + * Mi Band firmware version identifier + */ public final int fwVersion; public final int hwVersion; public final int feature; public final int appearance; + /** + * Heart rate firmware version identifier + */ + public final int fw2Version; + private boolean test1AHRMode; private boolean isChecksumCorrect(byte[] data) { @@ -29,6 +35,15 @@ public class DeviceInfo extends AbstractInfo { hwVersion = data[6] & 255; appearance = data[5] & 255; feature = data[4] & 255; + if (data.length == 20) { + int s = 0; + for (int i = 0; i < 4; ++i) { + s |= (data[16 + i] & 255) << i * 8; + } + fw2Version = s; + } else { + fw2Version = -1; + } } else { deviceId = "crc error"; profileVersion = -1; @@ -36,12 +51,13 @@ public class DeviceInfo extends AbstractInfo { hwVersion = -1; feature = -1; appearance = -1; + fw2Version = -1; } } public static int getInt(byte[] data, int from, int len) { int ret = 0; - for(int i = 0; i < len; ++i) { + for (int i = 0; i < len; ++i) { ret |= (data[from + i] & 255) << i * 8; } return ret; @@ -51,21 +67,25 @@ public class DeviceInfo extends AbstractInfo { return getInt(data, from, 4); } - public String getHumanFirmwareVersion() { - if (fwVersion == -1) - return GBApplication.getContext().getString(R.string._unknown_); - - return String.format("%d.%d.%d.%d", - fwVersion >> 24 & 255, - fwVersion >> 16 & 255, - fwVersion >> 8 & 255, - fwVersion & 255); - } - public int getFirmwareVersion() { return fwVersion; } + public int getHeartrateFirmwareVersion() { + if (test1AHRMode) { + return fwVersion; + } + return fw2Version; + } + + public void setTest1AHRMode(boolean enableTestMode) { + test1AHRMode = enableTestMode; + } + + public boolean supportsHeartrate() { + return isMili1S() || (test1AHRMode && isMili1A()); + } + @Override public String toString() { return "DeviceInfo{" + @@ -75,6 +95,7 @@ public class DeviceInfo extends AbstractInfo { ", hwVersion=" + hwVersion + ", feature=" + feature + ", appearance=" + appearance + + ", fw2Version (hr)=" + fw2Version + '}'; } @@ -86,9 +107,13 @@ public class DeviceInfo extends AbstractInfo { return feature == 5 && appearance == 0 || feature == 0 && hwVersion == 208; } - public boolean isMilli1S() { + public boolean isMili1S() { // TODO: this is probably not quite correct, but hopefully sufficient for early 1S support - return feature == 4 && appearance == 0 || feature == 4 && hwVersion == 4; + return (feature == 4 && appearance == 0) || hwVersion == 4; + } + + public boolean isAmazFit() { + return hwVersion == 6; } public String getHwVersion() { @@ -98,9 +123,12 @@ public class DeviceInfo extends AbstractInfo { if (isMili1A()) { return MiBandConst.MI_1A; } - if (isMilli1S()) { + if (isMili1S()) { return MiBandConst.MI_1S; } + if (isAmazFit()) { + return MiBandConst.MI_AMAZFIT; + } return "?"; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java new file mode 100644 index 000000000..ce975245c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java @@ -0,0 +1,37 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class Mi1AFirmwareInfo extends AbstractMi1FirmwareInfo { + private static final Logger LOG = LoggerFactory.getLogger(Mi1AFirmwareInfo.class); + + public static Mi1AFirmwareInfo getInstance(byte[] wholeFirmwareBytes) { + Mi1AFirmwareInfo info = new Mi1AFirmwareInfo(wholeFirmwareBytes); + if (info.isGenerallySupportedFirmware()) { + return info; + } + LOG.info("firmware not supported"); + return null; + } + + protected Mi1AFirmwareInfo(@NonNull byte[] wholeFirmwareBytes) { + super(wholeFirmwareBytes); + } + + @Override + protected int getSupportedMajorVersion() { + return 5; + } + + @Override + public boolean isGenerallyCompatibleWith(GBDevice device) { + String hwVersion = device.getHardwareVersion(); + return MiBandConst.MI_1A.equals(hwVersion); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java new file mode 100644 index 000000000..aa5067813 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java @@ -0,0 +1,37 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class Mi1FirmwareInfo extends AbstractMi1FirmwareInfo { + private static final Logger LOG = LoggerFactory.getLogger(Mi1FirmwareInfo.class); + + public static Mi1FirmwareInfo getInstance(byte[] wholeFirmwareBytes) { + Mi1FirmwareInfo info = new Mi1FirmwareInfo(wholeFirmwareBytes); + if (info.isGenerallySupportedFirmware()) { + return info; + } + LOG.info("firmware not supported"); + return null; + } + + protected Mi1FirmwareInfo(@NonNull byte[] wholeFirmwareBytes) { + super(wholeFirmwareBytes); + } + + @Override + protected int getSupportedMajorVersion() { + return 1; + } + + @Override + public boolean isGenerallyCompatibleWith(GBDevice device) { + String hwVersion = device.getHardwareVersion(); + return MiBandConst.MI_1.equals(hwVersion); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java new file mode 100644 index 000000000..926afca19 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java @@ -0,0 +1,55 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.support.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; + +/** + * FW1 is Mi Band firmware + * FW2 is heartrate firmware + */ +public class Mi1SFirmwareInfo extends CompositeMiFirmwareInfo { + private static final Logger LOG = LoggerFactory.getLogger(Mi1SFirmwareInfo.class); + + private static final byte[] DOUBLE_FW_HEADER = new byte[]{ + (byte) 0x78, + (byte) 0x75, + (byte) 0x63, + (byte) 0x6b + }; + private static final int DOUBLE_FW_HEADER_OFFSET = 0; + + private Mi1SFirmwareInfo(byte[] wholeFirmwareBytes) { + super(wholeFirmwareBytes, new Mi1SFirmwareInfoFW1(wholeFirmwareBytes), new Mi1SFirmwareInfoFW2(wholeFirmwareBytes)); + } + + @Override + public boolean isGenerallyCompatibleWith(GBDevice device) { + return MiBandConst.MI_1S.equals(device.getHardwareVersion()); + } + + @Override + public boolean isSingleMiBandFirmware() { + return false; + } + + 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); + } + + @Nullable + public static Mi1SFirmwareInfo getInstance(byte[] wholeFirmwareBytes) { + Mi1SFirmwareInfo info = new Mi1SFirmwareInfo(wholeFirmwareBytes); + if (info.isGenerallySupportedFirmware()) { + return info; + } + LOG.info("firmware not supported"); + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW1.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW1.java new file mode 100644 index 000000000..ff0ad86c8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW1.java @@ -0,0 +1,63 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FW1 is Mi Band firmware + * FW2 is heartrate firmware + */ +public class Mi1SFirmwareInfoFW1 extends AbstractMi1SFirmwareInfo { + private static final Logger LOG = LoggerFactory.getLogger(Mi1SFirmwareInfoFW1.class); + private static final int MI1S_FW_BASE_OFFSET = 1092; + + Mi1SFirmwareInfoFW1(@NonNull byte[] wholeFirmwareBytes) { + super(wholeFirmwareBytes); + } + + @Override + protected boolean isHeaderValid() { + return true; + } + + @Override + public int getFirmwareOffset() { + return (wholeFirmwareBytes[12] & 255) << 24 + | (wholeFirmwareBytes[13] & 255) << 16 + | (wholeFirmwareBytes[14] & 255) << 8 + | (wholeFirmwareBytes[15] & 255); + } + + @Override + public int getFirmwareLength() { + return (wholeFirmwareBytes[16] & 255) << 24 + | (wholeFirmwareBytes[17] & 255) << 16 + | (wholeFirmwareBytes[18] & 255) << 8 + | (wholeFirmwareBytes[19] & 255); + } + + @Override + public int getFirmwareVersion() { + return (wholeFirmwareBytes[8] & 255) << 24 + | (wholeFirmwareBytes[9] & 255) << 16 + | (wholeFirmwareBytes[10] & 255) << 8 + | wholeFirmwareBytes[11] & 255; + } + + @Override + protected boolean isGenerallySupportedFirmware() { + try { + int majorVersion = getFirmwareVersionMajor(); + if (majorVersion == 4) { + return true; + } else { + LOG.warn("Only major version 4 is supported for 1S fw1: " + majorVersion); + } + } catch (IllegalArgumentException ex) { + LOG.warn("not supported 1S firmware 1: " + ex.getLocalizedMessage(), ex); + } + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW2.java new file mode 100644 index 000000000..febec98e6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW2.java @@ -0,0 +1,62 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FW1 is Mi Band firmware + * FW2 is heartrate firmware + */ +public class Mi1SFirmwareInfoFW2 extends AbstractMi1SFirmwareInfo { + private static final Logger LOG = LoggerFactory.getLogger(Mi1SFirmwareInfoFW2.class); + + Mi1SFirmwareInfoFW2(@NonNull byte[] wholeFirmwareBytes) { + super(wholeFirmwareBytes); + } + + @Override + protected boolean isHeaderValid() { + return true; + } + + @Override + public int getFirmwareOffset() { + return (wholeFirmwareBytes[26] & 255) << 24 + | (wholeFirmwareBytes[27] & 255) << 16 + | (wholeFirmwareBytes[28] & 255) << 8 + | (wholeFirmwareBytes[29] & 255); + } + + @Override + public int getFirmwareLength() { + return (wholeFirmwareBytes[30] & 255) << 24 + | (wholeFirmwareBytes[31] & 255) << 16 + | (wholeFirmwareBytes[32] & 255) << 8 + | (wholeFirmwareBytes[33] & 255); + } + + @Override + protected boolean isGenerallySupportedFirmware() { + try { + int majorVersion = getFirmwareVersionMajor(); + if (majorVersion == 1) { + return true; + } else { + LOG.warn("Only major version 1 is supported for 1S fw2: " + majorVersion); + } + } catch (IllegalArgumentException ex) { + LOG.warn("not supported 1S firmware 2: " + ex.getLocalizedMessage(), ex); + } + return false; + } + + @Override + public int getFirmwareVersion() { + return (wholeFirmwareBytes[22] & 255) << 24 + | (wholeFirmwareBytes[23] & 255) << 16 + | (wholeFirmwareBytes[24] & 255) << 8 + | wholeFirmwareBytes[25] & 255; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java index a78b37f86..2d97818d7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java @@ -3,9 +3,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.Intent; -import android.content.SharedPreferences; import android.net.Uri; -import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.widget.Toast; @@ -21,6 +19,7 @@ import java.util.GregorianCalendar; import java.util.List; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; @@ -30,25 +29,29 @@ 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.impl.GBAlarm; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; -import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; 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.devices.miband.operations.FetchActivityOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.UpdateFirmwareOperation; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COUNT; @@ -76,6 +79,11 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ge public class MiBandSupport extends AbstractBTLEDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(MiBandSupport.class); + /** + * This is just for temporary testing of Mi1A double firmware update. + * DO NOT SET TO TRUE UNLESS YOU KNOW WHAT YOU'RE DOING! + */ + public static final boolean MI_1A_HR_FW_UPDATE_TEST_MODE_ENABLED = false; private volatile boolean telephoneRinging; private volatile boolean isLocatingDevice; @@ -88,25 +96,51 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); + addSupportedService(MiBandService.UUID_SERVICE_HEART_RATE); addSupportedService(GattService.UUID_SERVICE_IMMEDIATE_ALERT); } @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZING, getContext())); - pair(builder) + enableNotifications(builder, true) + .setLowLatency(builder) + .readDate(builder) // without reading the data, we get sporadic connection problems, especially directly after turning on BT + .pair(builder) .requestDeviceInfo(builder) .sendUserInfo(builder) + .checkAuthenticationNeeded(builder, getDevice()) .setWearLocation(builder) + .setHeartrateSleepSupport(builder) .setFitnessGoal(builder) - .enableNotifications(builder, true) + .enableFurtherNotifications(builder, true) .setCurrentTime(builder) .requestBatteryInfo(builder) + .setHighLatency(builder) .setInitialized(builder); - return builder; } + private MiBandSupport readDate(TransactionBuilder builder) { + builder.read(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_DATE_TIME)); + return this; + } + + public MiBandSupport setLowLatency(TransactionBuilder builder) { + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getLowLatency()); + return this; + } + + public MiBandSupport setHighLatency(TransactionBuilder builder) { + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getHighLatency()); + return this; + } + + private MiBandSupport checkAuthenticationNeeded(TransactionBuilder builder, GBDevice device) { + builder.add(new CheckAuthenticationNeededAction(device)); + return this; + } + /** * Last action of initialization sequence. Sets the device to initialized. * It is only invoked if all other actions were successfully run, so the device @@ -120,11 +154,20 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { // TODO: tear down the notifications on quit private MiBandSupport enableNotifications(TransactionBuilder builder, boolean enable) { - builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_NOTIFICATION), enable) - .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable) + builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_NOTIFICATION), enable); + return this; + } + + private MiBandSupport 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 + BluetoothGattCharacteristic heartrateCharacteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT); + if (heartrateCharacteristic != null) { + builder.notify(heartrateCharacteristic, enable); + } return this; } @@ -187,6 +230,14 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } static final byte[] reboot = new byte[]{MiBandService.COMMAND_REBOOT}; + + static final byte[] startHeartMeasurementManual = new byte[]{0x15, MiBandService.COMMAND_SET_HR_MANUAL, 1}; + static final byte[] stopHeartMeasurementManual = new byte[]{0x15, MiBandService.COMMAND_SET_HR_MANUAL, 0}; + static final byte[] startHeartMeasurementContinuous = new byte[]{0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 1}; + static final byte[] stopHeartMeasurementContinuous = new byte[]{0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 0}; + static final byte[] startHeartMeasurementSleep = new byte[]{0x15, MiBandService.COMMAND_SET_HR_SLEEP, 1}; + static final byte[] stopHeartMeasurementSleep = new byte[]{0x15, MiBandService.COMMAND_SET_HR_SLEEP, 0}; + static final byte[] startRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 1}; static final byte[] stopRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 0}; @@ -233,6 +284,31 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { return this; } + /* private MiBandSupport requestHRInfo(TransactionBuilder builder) { + LOG.debug("Requesting HR Info!"); + BluetoothGattCharacteristic HRInfo = getCharacteristic(MiBandService.UUID_CHAR_HEART_RATE_MEASUREMENT); + builder.read(HRInfo); + BluetoothGattCharacteristic HR_Point = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT); + builder.read(HR_Point); + return this; + } + *//** + * Part of HR test. Do not call manually. + * + * @param transaction + * @return + *//* + private MiBandSupport heartrate(TransactionBuilder transaction) { + LOG.info("Attempting to read HR ..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHAR_HEART_RATE_MEASUREMENT); + if (characteristic != null) { + transaction.write(characteristic, new byte[]{MiBandService.COMMAND_SET__HR_CONTINUOUS}); + } else { + LOG.info("Unable to read HR from MI device -- characteristic not available"); + } + return this; + }*/ + /** * Part of device initialization process. Do not call manually. * @@ -295,6 +371,44 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { return this; } + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + try { + TransactionBuilder builder = performInitialized("enable heart rate sleep support: " + enable); + setHeartrateSleepSupport(builder); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error toggling heart rate sleep support: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + /** + * Part of device initialization process. Do not call manually. + * + * @param builder + */ + private MiBandSupport 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; + } + } + }); + } + return this; + } + private void performDefaultNotification(String task, short repeat, BtLEAction extraAction) { try { TransactionBuilder builder = performInitialized(task); @@ -308,7 +422,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { private void performPreferredNotification(String task, String notificationOrigin, BtLEAction extraAction) { try { TransactionBuilder builder = performInitialized(task); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + Prefs prefs = GBApplication.getPrefs(); int vibrateDuration = getPreferredVibrateDuration(notificationOrigin, prefs); int vibratePause = getPreferredVibratePause(notificationOrigin, prefs); short vibrateTimes = getPreferredVibrateCount(notificationOrigin, prefs); @@ -327,35 +441,35 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } } - private int getPreferredFlashDuration(String notificationOrigin, SharedPreferences prefs) { + private int getPreferredFlashDuration(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(FLASH_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_DURATION); } - private int getPreferredOriginalColour(String notificationOrigin, SharedPreferences prefs) { + private int getPreferredOriginalColour(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(FLASH_ORIGINAL_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR); } - private int getPreferredFlashColour(String notificationOrigin, SharedPreferences prefs) { + private int getPreferredFlashColour(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(FLASH_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COLOUR); } - private int getPreferredFlashCount(String notificationOrigin, SharedPreferences prefs) { + private int getPreferredFlashCount(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(FLASH_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COUNT); } - private int getPreferredVibratePause(String notificationOrigin, SharedPreferences prefs) { + private int getPreferredVibratePause(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(VIBRATION_PAUSE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PAUSE); } - private short getPreferredVibrateCount(String notificationOrigin, SharedPreferences prefs) { + private short getPreferredVibrateCount(String notificationOrigin, Prefs prefs) { return (short) Math.min(Short.MAX_VALUE, getNotificationPrefIntValue(VIBRATION_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_COUNT)); } - private int getPreferredVibrateDuration(String notificationOrigin, SharedPreferences prefs) { + private int getPreferredVibrateDuration(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(VIBRATION_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_DURATION); } - private VibrationProfile getPreferredVibrateProfile(String notificationOrigin, SharedPreferences prefs, short repeat) { + private VibrationProfile getPreferredVibrateProfile(String notificationOrigin, Prefs prefs, short repeat) { String profileId = getNotificationPrefStringValue(VIBRATION_PROFILE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PROFILE); return VibrationProfile.getProfile(profileId, repeat); } @@ -446,8 +560,8 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } @Override - public void onSetCallState(String number, String name, ServiceCommand command) { - if (ServiceCommand.CALL_INCOMING.equals(command)) { + public void onSetCallState(CallSpec callSpec) { + if (callSpec.command == CallSpec.CALL_INCOMING) { telephoneRinging = true; AbortTransactionAction abortAction = new AbortTransactionAction() { @Override @@ -456,7 +570,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } }; performPreferredNotification("incoming call", MiBandConst.ORIGIN_INCOMING_CALL, abortAction); - } else if (ServiceCommand.CALL_START.equals(command) || ServiceCommand.CALL_END.equals(command)) { + } else if ((callSpec.command == CallSpec.CALL_START) || (callSpec.command == CallSpec.CALL_END)) { telephoneRinging = false; } } @@ -467,7 +581,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } @Override - public void onSetMusicInfo(String artist, String album, String track) { + public void onSetMusicInfo(MusicSpec musicSpec) { // not supported } @@ -482,6 +596,45 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } } + @Override + public void onHeartRateTest() { + if (supportsHeartRate()) { + 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 in MI1S", ex); + } + } else { + GB.toast(getContext(), "Heart rate is not supported on this device", Toast.LENGTH_LONG, GB.ERROR); + } + } + + @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); + } + } + } + + public boolean supportsHeartRate() { + return getDeviceInfo() != null && getDeviceInfo().supportsHeartrate(); + } + @Override public void onFindDevice(boolean start) { isLocatingDevice = start; @@ -584,6 +737,11 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { // not supported } + @Override + public void onAppConfiguration(UUID uuid, String config) { + // not supported + } + @Override public void onScreenshotReq() { // not supported @@ -601,6 +759,10 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { handleNotificationNotif(characteristic.getValue()); } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) { handleRealtimeSteps(characteristic.getValue()); + } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) { + handleRealtimeSteps(characteristic.getValue()); + } else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { + handleHeartrate(characteristic.getValue()); } else { LOG.info("Unhandled characteristic changed: " + characteristicUUID); logMessageContent(characteristic.getValue()); @@ -619,10 +781,12 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { handleDeviceName(characteristic.getValue(), status); } else if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) { handleBatteryInfo(characteristic.getValue(), status); - } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) { - handleRealtimeSteps(characteristic.getValue()); + } else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { + logHeartrate(characteristic.getValue()); + } else if (MiBandService.UUID_CHARACTERISTIC_DATE_TIME.equals(characteristicUUID)) { + logDate(characteristic.getValue()); } else { - LOG.info("Unhandled characteristic read: "+ characteristicUUID); + LOG.info("Unhandled characteristic read: " + characteristicUUID); logMessageContent(characteristic.getValue()); } } @@ -652,7 +816,30 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } } + public void logDate(byte[] value) { + GregorianCalendar calendar = MiBandDateConverter.rawBytesToCalendar(value); + LOG.info("Got Mi Band Date: " + DateTimeUtils.formatDateTime(calendar.getTime())); + } + public void logHeartrate(byte[] value) { + LOG.info("Got heartrate:"); + if (value.length == 2 && value[0] == 6) { + int hrValue = (value[1] & 0xff); + GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO); + } else { + logMessageContent(value); + } + } + + private void handleHeartrate(byte[] value) { + if (value.length == 2 && value[0] == 6) { + int hrValue = (value[1] & 0xff); + 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); + } + } private void handleRealtimeSteps(byte[] value) { int steps = 0xff & value[0] | (0xff & value[1]) << 8; @@ -685,6 +872,27 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { return; } switch (value[0]) { + case MiBandService.NOTIFY_AUTHENTICATION_FAILED: + // we get first FAILED, then NOTIFY_STATUS_MOTOR_AUTH (0x13) + // which means, we need to authenticate by tapping + getDevice().setState(State.AUTHENTICATION_REQUIRED); + getDevice().sendDeviceUpdateIntent(getContext()); + GB.toast(getContext(), "Band needs pairing", Toast.LENGTH_LONG, GB.ERROR); + break; + case MiBandService.NOTIFY_AUTHENTICATION_SUCCESS: // fall through -- not sure which one we get + case MiBandService.NOTIFY_RESET_AUTHENTICATION_SUCCESS: // for Mi 1A + case MiBandService.NOTIFY_STATUS_MOTOR_AUTH_SUCCESS: + LOG.info("Band successfully authenticated"); + // maybe we can perform the rest of the initialization from here + doInitialize(); + break; + + case MiBandService.NOTIFY_STATUS_MOTOR_AUTH: + LOG.info("Band needs authentication (MOTOR_AUTH)"); + getDevice().setState(State.AUTHENTICATING); + getDevice().sendDeviceUpdateIntent(getContext()); + break; + default: for (byte b : value) { LOG.warn("DATA: " + String.format("0x%2x", b)); @@ -692,12 +900,27 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } } + private void doInitialize() { + try { + TransactionBuilder builder = performInitialized("just initializing after authentication"); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to initialize device after authentication", ex); + } + } + private void handleDeviceInfo(byte[] value, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { mDeviceInfo = new DeviceInfo(value); + mDeviceInfo.setTest1AHRMode(MI_1A_HR_FW_UPDATE_TEST_MODE_ENABLED); + if (getDeviceInfo().supportsHeartrate()) { + getDevice().addDeviceInfo(new GenericItem( + getContext().getString(R.string.DEVINFO_HR_VER), + MiBandFWHelper.formatFirmwareVersion(mDeviceInfo.getHeartrateFirmwareVersion()))); + } LOG.warn("Device info: " + mDeviceInfo); versionCmd.hwVersion = mDeviceInfo.getHwVersion(); - versionCmd.fwVersion = mDeviceInfo.getHumanFirmwareVersion(); + versionCmd.fwVersion = MiBandFWHelper.formatFirmwareVersion(mDeviceInfo.getFirmwareVersion()); handleGBDeviceEvent(versionCmd); } } @@ -740,7 +963,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { if (status != BluetoothGatt.GATT_SUCCESS) { LOG.warn("Could not write to the control point."); } - LOG.info("handleControlPoint write status:" + status); + LOG.info("handleControlPoint write status:" + status + "; length: " + (value != null ? value.length : "(null)")); if (value != null) { for (byte b : value) { @@ -763,10 +986,12 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } private void handleUserInfoResult(byte[] value, int status) { - // successfully transfered user info means we're initialized - if (status == BluetoothGatt.GATT_SUCCESS) { - setConnectionState(State.INITIALIZED); - } + // successfully transferred user info means we're initialized +// commented out, because we have SetDeviceStateAction which sets initialized +// state on every successful initialization. +// if (status == BluetoothGatt.GATT_SUCCESS) { +// setConnectionState(State.INITIALIZED); +// } } private void setConnectionState(State newState) { @@ -806,16 +1031,15 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { TransactionBuilder builder = performInitialized("Send upcoming events"); BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - int availableSlots = Integer.parseInt(prefs.getString(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 mEvents = upcomingEvents.getCalendarEventList(getContext()); + List mEvents = upcomingEvents.getCalendarEventList(getContext()); int iteration = 0; - ArrayList alarmList = new ArrayList<>(); - for(CalendarEvents.CalendarEvent mEvt : mEvents) { + for (CalendarEvents.CalendarEvent mEvt : mEvents) { if (iteration >= availableSlots || iteration > 2) { break; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/TestMi1AFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/TestMi1AFirmwareInfo.java new file mode 100644 index 000000000..9df8b7782 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/TestMi1AFirmwareInfo.java @@ -0,0 +1,76 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.support.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +/** + * This is a class just for testing the dual fw firmware update procedure. + * It uses two instances of the known-to-be-working Mi1A firmware update instances + * and combines them in a CompositeMiFirmwareInfo. + *

+ * Most methods simply delegate to one of the child instances (FW1). + *

+ * FW1 is the default Mi 1A Band firmware + * FW2 is the same default Mi 1A Band firmware + */ +public class TestMi1AFirmwareInfo extends CompositeMiFirmwareInfo { + private static final Logger LOG = LoggerFactory.getLogger(TestMi1AFirmwareInfo.class); + + private TestMi1AFirmwareInfo(byte[] wholeFirmwareBytes) { + super(wholeFirmwareBytes, new Mi1AFirmwareInfo(wholeFirmwareBytes), new Mi1AFirmwareInfo(wholeFirmwareBytes)); + } + + @Override + public void checkValid() throws IllegalArgumentException { +// super.checkValid(); +// unfortunately we cannot use all of the checks in the superclass, so we roll our own + + if (getFirst().getFirmwareOffset() != getSecond().getFirmwareOffset()) { + throw new IllegalArgumentException("Test firmware offsets should be the same: " + getLengthsOffsetsString()); + } + if (getFirst().getFirmwareOffset() < 0 || getSecond().getFirmwareOffset() < 0 + || getFirst().getFirmwareLength() <= 0 || getSecond().getFirmwareLength() <= 0) { + throw new IllegalArgumentException("Illegal test firmware offsets/lengths: " + getLengthsOffsetsString()); + } + + if (getFirst().getFirmwareLength() != getSecond().getFirmwareLength()) { + throw new IllegalArgumentException("Illegal test firmware lengths: " + getLengthsOffsetsString()); + } + int firstEndIndex = getFirst().getFirmwareOffset() + getFirst().getFirmwareLength(); + int secondEndIndex = getSecond().getFirmwareOffset(); + if (wholeFirmwareBytes.length < firstEndIndex || wholeFirmwareBytes.length < secondEndIndex) { + throw new IllegalArgumentException("Invalid test firmware size, or invalid test offsets/lengths: " + getLengthsOffsetsString()); + } + + getFirst().checkValid(); + getSecond().checkValid(); + } + + @Override + public boolean isGenerallyCompatibleWith(GBDevice device) { + return getFirst().isGenerallyCompatibleWith(device); + } + + @Override + public boolean isSingleMiBandFirmware() { + return false; + } + + protected boolean isHeaderValid() { + return getFirst().isHeaderValid(); + } + + @Nullable + public static TestMi1AFirmwareInfo getInstance(byte[] wholeFirmwareBytes) { + TestMi1AFirmwareInfo info = new TestMi1AFirmwareInfo(wholeFirmwareBytes); + if (info.isGenerallySupportedFirmware()) { + return info; + } + LOG.info("firmware not supported"); + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java index ffabce535..2f6577d7f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java @@ -41,8 +41,9 @@ public abstract class AbstractMiBandOperation extends AbstractBTLEOperation= value.length; } @@ -127,10 +132,13 @@ public class FetchActivityOperation extends AbstractMiBandOperation { } } - private ActivityStruct activityStruct = new ActivityStruct(); + private ActivityStruct activityStruct; public FetchActivityOperation(MiBandSupport support) { super(support); + hasExtendedActivityData = support.getDeviceInfo().supportsHeartrate(); + activityDataHolderSize = getBytesPerMinuteOfActivityData() * 60 * 4; // 4h + activityStruct = new ActivityStruct(activityDataHolderSize); } @Override @@ -138,7 +146,7 @@ public class FetchActivityOperation extends AbstractMiBandOperation { // scheduleTaskExecutor = Executors.newScheduledThreadPool(1); TransactionBuilder builder = performInitialized("fetch activity data"); -// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getLowLatency()); + getSupport().setLowLatency(builder); builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext())); builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), fetch); builder.queue(getQueue()); @@ -208,30 +216,34 @@ public class FetchActivityOperation extends AbstractMiBandOperation { // counter of all data held by the band int totalDataToRead = (value[7] & 0xff) | ((value[8] & 0xff) << 8); - totalDataToRead *= (dataType == MiBandService.MODE_REGULAR_DATA_LEN_MINUTE) ? 3 : 1; + totalDataToRead *= (dataType == MiBandService.MODE_REGULAR_DATA_LEN_MINUTE) ? getBytesPerMinuteOfActivityData() : 1; // counter of this data block int dataUntilNextHeader = (value[9] & 0xff) | ((value[10] & 0xff) << 8); - dataUntilNextHeader *= (dataType == MiBandService.MODE_REGULAR_DATA_LEN_MINUTE) ? 3 : 1; + dataUntilNextHeader *= (dataType == MiBandService.MODE_REGULAR_DATA_LEN_MINUTE) ? getBytesPerMinuteOfActivityData() : 1; - // there is a total of totalDataToRead that will come in chunks (3 bytes per minute if dataType == 1 (MiBandService.MODE_REGULAR_DATA_LEN_MINUTE)), + // there is a total of totalDataToRead that will come in chunks (3 or 4 bytes per minute if dataType == 1 (MiBandService.MODE_REGULAR_DATA_LEN_MINUTE)), // these chunks are usually 20 bytes long and grouped in blocks // after dataUntilNextHeader bytes we will get a new packet of 11 bytes that should be parsed // as we just did if (activityStruct.isFirstChunk() && dataUntilNextHeader != 0) { GB.toast(getContext().getString(R.string.user_feedback_miband_activity_data_transfer, - DateTimeUtils.formatDurationHoursMinutes((totalDataToRead / 3), TimeUnit.MINUTES), + DateTimeUtils.formatDurationHoursMinutes((totalDataToRead / getBytesPerMinuteOfActivityData()), TimeUnit.MINUTES), DateFormat.getDateTimeInstance().format(timestamp.getTime())), Toast.LENGTH_LONG, GB.INFO); } - LOG.info("total data to read: " + totalDataToRead + " len: " + (totalDataToRead / 3) + " minute(s)"); - LOG.info("data to read until next header: " + dataUntilNextHeader + " len: " + (dataUntilNextHeader / 3) + " minute(s)"); + LOG.info("total data to read: " + totalDataToRead + " len: " + (totalDataToRead / getBytesPerMinuteOfActivityData()) + " minute(s)"); + LOG.info("data to read until next header: " + dataUntilNextHeader + " len: " + (dataUntilNextHeader / getBytesPerMinuteOfActivityData()) + " minute(s)"); LOG.info("TIMESTAMP: " + DateFormat.getDateTimeInstance().format(timestamp.getTime()) + " magic byte: " + dataUntilNextHeader); activityStruct.startNewBlock(timestamp, dataUntilNextHeader); } + private int getBytesPerMinuteOfActivityData() { + return hasExtendedActivityData ? 4 : 3; + } + /** * Method to store temporarily the activity data values got from the Mi Band. *

@@ -290,8 +302,9 @@ public class FetchActivityOperation extends AbstractMiBandOperation { LOG.debug("nothing to flush, struct is already null"); return; } - LOG.debug("flushing activity data samples: " + activityStruct.activityDataHolderProgress / 3); - byte category, intensity, steps; + int bpm = getBytesPerMinuteOfActivityData(); + LOG.debug("flushing activity data samples: " + activityStruct.activityDataHolderProgress / bpm); + byte category, intensity, steps, heartrate = 0; DBHandler dbHandler = null; try { @@ -299,25 +312,29 @@ public class FetchActivityOperation extends AbstractMiBandOperation { int minutes = 0; try (SQLiteDatabase db = dbHandler.getWritableDatabase()) { // explicitly keep the db open while looping over the samples int timestampInSeconds = (int) (activityStruct.activityDataTimestampProgress.getTimeInMillis() / 1000); - if ((activityStruct.activityDataHolderProgress % 3) != 0) { - throw new IllegalStateException("Unexpected data, progress should be mutiple of 3: " + activityStruct.activityDataHolderProgress); + if ((activityStruct.activityDataHolderProgress % bpm) != 0) { + throw new IllegalStateException("Unexpected data, progress should be mutiple of " + bpm + ": " + activityStruct.activityDataHolderProgress); } - int numSamples = activityStruct.activityDataHolderProgress/3; + int numSamples = activityStruct.activityDataHolderProgress / bpm; ActivitySample[] samples = new ActivitySample[numSamples]; SampleProvider sampleProvider = new MiBandSampleProvider(); - int s = 0; - for (int i = 0; i < activityStruct.activityDataHolderProgress; i += 3) { + for (int i = 0; i < activityStruct.activityDataHolderProgress; i += bpm) { category = activityStruct.activityDataHolder[i]; intensity = activityStruct.activityDataHolder[i + 1]; steps = activityStruct.activityDataHolder[i + 2]; + if (hasExtendedActivityData) { + heartrate = activityStruct.activityDataHolder[i + 3]; + LOG.debug("heartrate received: " + (heartrate & 0xff)); + } samples[minutes] = new GBActivitySample( sampleProvider, timestampInSeconds, - (short) (intensity & 0xff), - (short) (steps & 0xff), - category); + intensity & 0xff, + steps & 0xff, + category & 0xff, + heartrate & 0xff); // next minute minutes++; @@ -347,7 +364,7 @@ public class FetchActivityOperation extends AbstractMiBandOperation { */ private void sendAckDataTransfer(Calendar time, int bytesTransferred) { byte[] ackTime = MiBandDateConverter.calendarToRawBytes(time); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(GBApplication.getContext()); + Prefs prefs = GBApplication.getPrefs(); byte[] ackChecksum = new byte[]{ (byte) (bytesTransferred & 0xff), @@ -385,6 +402,7 @@ public class FetchActivityOperation extends AbstractMiBandOperation { if (prefs.getBoolean(MiBandConst.PREF_MIBAND_DONT_ACK_TRANSFER, false)) { builder = performInitialized("send acknowledge"); builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), new byte[]{MiBandService.COMMAND_STOP_SYNC_DATA}); + getSupport().setHighLatency(builder); builder.queue(getQueue()); } handleActivityFetchFinish(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/UpdateFirmwareOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/UpdateFirmwareOperation.java index 3108359ef..32185d832 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/UpdateFirmwareOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/UpdateFirmwareOperation.java @@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; import android.net.Uri; import android.widget.Toast; @@ -12,23 +13,28 @@ import java.io.IOException; import java.util.Arrays; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.PlainAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.AbstractMiFirmwareInfo; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class UpdateFirmwareOperation extends AbstractMiBandOperation { private static final Logger LOG = LoggerFactory.getLogger(UpdateFirmwareOperation.class); private final Uri uri; private boolean firmwareInfoSent = false; - private byte[] newFirmware; - private boolean rebootWhenBandReady = false; + private UpdateCoordinator updateCoordinator; + final Prefs prefs = GBApplication.getPrefs(); public UpdateFirmwareOperation(Uri uri, MiBandSupport support) { super(support); @@ -38,20 +44,29 @@ public class UpdateFirmwareOperation extends AbstractMiBandOperation { @Override protected void doPerform() throws IOException { MiBandFWHelper mFwHelper = new MiBandFWHelper(uri, getContext()); - String mMac = getDevice().getAddress(); - String[] mMacOctets = mMac.split(":"); - int newFwVersion = mFwHelper.getFirmwareVersion(); - int oldFwVersion = getSupport().getDeviceInfo().getFirmwareVersion(); - int checksum = (Integer.decode("0x" + mMacOctets[4]) << 8 | Integer.decode("0x" + mMacOctets[5])) ^ CheckSums.getCRC16(mFwHelper.getFw()); + AbstractMiFirmwareInfo firmwareInfo = mFwHelper.getFirmwareInfo(); + if (!firmwareInfo.isGenerallyCompatibleWith(getDevice())) { + throw new IOException("Firmware is not compatible with the given device: " + getDevice().getAddress()); + } - sendFirmwareInfo(oldFwVersion, newFwVersion, mFwHelper.getFw().length, checksum); - firmwareInfoSent = true; - newFirmware = mFwHelper.getFw(); + if (getSupport().supportsHeartRate()) { + updateCoordinator = prepareFirmwareInfo1S(firmwareInfo); + } else { + updateCoordinator = prepareFirmwareInfo(mFwHelper.getFw(), mFwHelper.getFirmwareVersion()); + } + + updateCoordinator.initNextOperation(); + if (!updateCoordinator.sendFwInfo()) { + displayMessage(getContext(), "Error sending firmware info, aborting.", Toast.LENGTH_LONG, GB.ERROR); + done(); + } //the firmware will be sent by the notification listener if the band confirms that the metadata are ok. } private void done() { + LOG.info("Operation done."); + updateCoordinator = null; operationFinished(); unsetBusy(); } @@ -68,7 +83,7 @@ public class UpdateFirmwareOperation extends AbstractMiBandOperation { } /** - * React to unsolicited messages sent by the Mi Band to the MiBandService.UUID_CHARACTERISTIC_NOTIFICATION + * React to messages sent by the Mi Band to the MiBandService.UUID_CHARACTERISTIC_NOTIFICATION * characteristic, * These messages appear to be always 1 byte long, with values that are listed in MiBandService. * It is not excluded that there are further values which are still unknown. @@ -83,40 +98,51 @@ public class UpdateFirmwareOperation extends AbstractMiBandOperation { getSupport().logMessageContent(value); return; } + if (updateCoordinator == null) { + LOG.error("received notification when updateCoordinator is null, ignoring (notification content follows):"); + getSupport().logMessageContent(value); + return; + } + switch (value[0]) { case MiBandService.NOTIFY_FW_CHECK_SUCCESS: - if (firmwareInfoSent && newFirmware != null) { - if (sendFirmwareData(newFirmware)) { - rebootWhenBandReady = true; - } else { - //TODO: the firmware transfer failed, but the miband should be still functional with the old firmware. What should we do? - GB.toast(getContext().getString(R.string.updatefirmwareoperation_updateproblem_do_not_reboot), Toast.LENGTH_LONG, GB.ERROR); + if (firmwareInfoSent) { + displayMessage(getContext(), "Firmware metadata successfully sent.", Toast.LENGTH_LONG, GB.INFO); + if (!updateCoordinator.sendFwData()) { + displayMessage(getContext(), getContext().getString(R.string.updatefirmwareoperation_updateproblem_do_not_reboot), Toast.LENGTH_LONG, GB.ERROR); done(); } firmwareInfoSent = false; - newFirmware = null; + } else { + LOG.warn("firmwareInfoSent is false -- not sending firmware data even though we got meta data success notification"); } break; case MiBandService.NOTIFY_FW_CHECK_FAILED: - GB.toast(getContext().getString(R.string.updatefirmwareoperation_metadata_updateproblem), Toast.LENGTH_LONG, GB.ERROR); + displayMessage(getContext(), getContext().getString(R.string.updatefirmwareoperation_metadata_updateproblem), Toast.LENGTH_LONG, GB.ERROR); firmwareInfoSent = false; - newFirmware = null; done(); break; case MiBandService.NOTIFY_FIRMWARE_UPDATE_SUCCESS: - if (rebootWhenBandReady) { - GB.toast(getContext(), getContext().getString(R.string.updatefirmwareoperation_update_complete_rebooting), Toast.LENGTH_LONG, GB.INFO); + if (updateCoordinator.initNextOperation()) { + displayMessage(getContext(), "Heart Rate Firmware successfully updated, now updating Mi Band Firmware", Toast.LENGTH_LONG, GB.INFO); + if (!updateCoordinator.sendFwInfo()) { + displayMessage(getContext(), "Error sending firmware info, aborting.", Toast.LENGTH_LONG, GB.ERROR); + done(); + } + break; + } else if (updateCoordinator.needsReboot()) { + displayMessage(getContext(), getContext().getString(R.string.updatefirmwareoperation_update_complete_rebooting), Toast.LENGTH_LONG, GB.INFO); GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_update_complete), false, 100, getContext()); getSupport().onReboot(); - rebootWhenBandReady = false; + } else { + LOG.error("BUG: Successful firmware update without reboot???"); } done(); break; case MiBandService.NOTIFY_FIRMWARE_UPDATE_FAILED: //TODO: the firmware transfer failed, but the miband should be still functional with the old firmware. What should we do? - GB.toast(getContext().getString(R.string.updatefirmwareoperation_updateproblem_do_not_reboot), Toast.LENGTH_LONG, GB.ERROR); + displayMessage(getContext(), getContext().getString(R.string.updatefirmwareoperation_updateproblem_do_not_reboot), Toast.LENGTH_LONG, GB.ERROR); GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_write_failed), false, 0, getContext()); - rebootWhenBandReady = false; done(); break; @@ -126,6 +152,10 @@ public class UpdateFirmwareOperation extends AbstractMiBandOperation { } } + private void displayMessage(Context context, String message, int duration, int severity) { + getSupport().handleGBDeviceEvent(new GBDeviceEventDisplayMessage(message, duration, severity)); + } + /** * Prepare the MiBand to receive the new firmware data. * Some information about the new firmware version have to be pushed to the MiBand before sending @@ -133,13 +163,70 @@ public class UpdateFirmwareOperation extends AbstractMiBandOperation { *

* The Mi Band will send a notification after receiving these data to confirm if the metadata looks good to it. * - * @param currentFwVersion * @param newFwVersion - * @param newFwSize - * @param checksum * @see MiBandSupport#handleNotificationNotif */ - private void sendFirmwareInfo(int currentFwVersion, int newFwVersion, int newFwSize, int checksum) throws IOException { + private UpdateCoordinator prepareFirmwareInfo(byte[] fwBytes, int newFwVersion) throws IOException { + int newFwSize = fwBytes.length; + String mMac = getDevice().getAddress(); + String[] mMacOctets = mMac.split(":"); + int currentFwVersion = getSupport().getDeviceInfo().getFirmwareVersion(); + int checksum = (Integer.decode("0x" + mMacOctets[4]) << 8 | Integer.decode("0x" + mMacOctets[5])) ^ CheckSums.getCRC16(fwBytes); + + byte[] fwInfo = prepareFirmwareUpdateA(currentFwVersion, newFwVersion, newFwSize, checksum); + return new SingleUpdateCoordinator(fwInfo, fwBytes, true); + } + + private UpdateCoordinator prepareFirmwareInfo1S(AbstractMiFirmwareInfo info) { + if (info.isSingleMiBandFirmware()) { + throw new IllegalArgumentException("preparing single fw not allowed for 1S"); + } + int fw2Version = info.getSecond().getFirmwareVersion(); + int fw1Version = info.getFirst().getFirmwareVersion(); + + String[] mMacOctets = getDevice().getAddress().split(":"); + int encodedMac = (Integer.decode("0x" + mMacOctets[4]) << 8 | Integer.decode("0x" + mMacOctets[5])); + + byte[] fw2Bytes = info.getSecond().getFirmwareBytes(); + int fw2Checksum = CheckSums.getCRC16(fw2Bytes) ^ encodedMac; + + byte[] fw1Bytes = info.getFirst().getFirmwareBytes(); + int fw1Checksum = encodedMac ^ CheckSums.getCRC16(fw1Bytes); + + // check firmware validity? + + int fw1OldVersion = getSupport().getDeviceInfo().getFirmwareVersion(); + int fw2OldVersion = getSupport().getDeviceInfo().getHeartrateFirmwareVersion(); + + boolean rebootWhenFinished = true; + if (info.isSingleMiBandFirmware()) { + LOG.info("is single Mi Band firmware"); + byte[] fw1Info = prepareFirmwareInfo(fw1Bytes, fw1OldVersion, fw1Version, fw1Checksum, 0, rebootWhenFinished /*, progress monitor */); + return new SingleUpdateCoordinator(fw1Info, fw1Bytes, rebootWhenFinished); + } else { + LOG.info("is multi Mi Band firmware, sending fw2 (hr) first"); + byte[] fw2Info = prepareFirmwareInfo(fw2Bytes, fw2OldVersion, fw2Version, fw2Checksum, 1, rebootWhenFinished /*, progress monitor */); + byte[] fw1Info = prepareFirmwareInfo(fw1Bytes, fw1OldVersion, fw1Version, fw1Checksum, 0, rebootWhenFinished /*, progress monitor */); + return new DoubleUpdateCoordinator(fw1Info, fw1Bytes, fw2Info, fw2Bytes, rebootWhenFinished); + } + } + + private byte[] prepareFirmwareInfo(byte[] fwBytes, int currentFwVersion, int newFwVersion, int checksum, int something, boolean reboot) { + byte[] fwInfo; + switch (something) { + case -1: + fwInfo = prepareFirmwareUpdateA(currentFwVersion, newFwVersion, fwBytes.length, checksum); + break; + case -2: + fwInfo = prepareFirmwareUpdateB(currentFwVersion, newFwVersion, fwBytes.length, checksum, 0); + break; + default: + fwInfo = prepareFirmwareUpdateB(currentFwVersion, newFwVersion, fwBytes.length, checksum, something); + } + return fwInfo; + } + + private byte[] prepareFirmwareUpdateA(int currentFwVersion, int newFwVersion, int newFwSize, int checksum) { byte[] fwInfo = new byte[]{ MiBandService.COMMAND_SEND_FIRMWARE_INFO, (byte) currentFwVersion, @@ -155,67 +242,227 @@ public class UpdateFirmwareOperation extends AbstractMiBandOperation { (byte) checksum, (byte) (checksum >> 8) }; - TransactionBuilder builder = performInitialized("send firmware info"); - builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.updating_firmware), getContext())); - builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), fwInfo); - builder.queue(getQueue()); + return fwInfo; } + private byte[] prepareFirmwareUpdateB(int currentFwVersion, int newFwVersion, int newFwSize, int checksum, int something) { + byte[] fwInfo = new byte[]{ + MiBandService.COMMAND_SEND_FIRMWARE_INFO, + (byte) currentFwVersion, + (byte) (currentFwVersion >> 8), + (byte) (currentFwVersion >> 16), + (byte) (currentFwVersion >> 24), + (byte) newFwVersion, + (byte) (newFwVersion >> 8), + (byte) (newFwVersion >> 16), + (byte) (newFwVersion >> 24), + (byte) newFwSize, + (byte) (newFwSize >> 8), + (byte) checksum, + (byte) (checksum >> 8), + (byte) something + }; + return fwInfo; + } + + /** - * Method that uploads a firmware (fwbytes) to the MiBand. - * The firmware has to be splitted into chunks of 20 bytes each, and periodically a COMMAND_SYNC comand has to be issued to the MiBand. + * Method that uploads a firmware (fwbytes) to the Mi Band. + * The firmware has to be split into chunks of 20 bytes each, and periodically a COMMAND_SYNC command has to be issued to the Mi Band. *

- * The Mi Band will send a notification after receiving these data to confirm if the firmware looks good to it. + * The Mi Band will send a notification after receiving this data to confirm if the firmware looks good to it. * * @param fwbytes * @return whether the transfer succeeded or not. Only a BT layer exception will cause the transmission to fail. * @see MiBandSupport#handleNotificationNotif */ - private boolean sendFirmwareData(byte fwbytes[]) { + private boolean sendFirmwareData(byte[] fwbytes) { int len = fwbytes.length; final int packetLength = 20; int packets = len / packetLength; - byte fwChunk[] = new byte[packetLength]; - - int firmwareProgress = 0; + BluetoothGattCharacteristic characteristicControlPoint = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); + BluetoothGattCharacteristic characteristicFWData = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_FIRMWARE_DATA); try { - TransactionBuilder builder = performInitialized("send firmware packet"); - for (int i = 0; i < packets; i++) { - fwChunk = Arrays.copyOfRange(fwbytes, i * packetLength, i * packetLength + packetLength); + // going from 0 to len + int firmwareProgress = 0; - builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_FIRMWARE_DATA), fwChunk); + TransactionBuilder builder = performInitialized("send firmware packet"); + if (prefs.getBoolean("mi_low_latency_fw_update", true)) { + getSupport().setLowLatency(builder); + } + for (int i = 0; i < packets; i++) { + byte[] fwChunk = Arrays.copyOfRange(fwbytes, i * packetLength, i * packetLength + packetLength); + + builder.write(characteristicFWData, fwChunk); firmwareProgress += packetLength; + int progressPercent = (int) ((((float) firmwareProgress) / len) * 100); if ((i > 0) && (i % 50 == 0)) { - builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), new byte[]{MiBandService.COMMAND_SYNC}); - builder.add(new SetProgressAction("Firmware update in progress", true, (int)(((float) firmwareProgress) / len * 100), getContext())); + builder.write(characteristicControlPoint, new byte[]{MiBandService.COMMAND_SYNC}); + builder.add(new SetProgressAction(getContext().getString(R.string.updatefirmwareoperation_update_in_progress), true, progressPercent, getContext())); } - - LOG.info("Firmware update progress:" + firmwareProgress + " total len:" + len + " progress:" + (int)(((float) firmwareProgress) / len * 100)); } - if (!(len % packetLength == 0)) { - byte lastChunk[] = new byte[len % packetLength]; - lastChunk = Arrays.copyOfRange(fwbytes, packets * packetLength, len); - builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_FIRMWARE_DATA), lastChunk); - firmwareProgress += len % packetLength; - } - - LOG.info("Firmware update progress:" + firmwareProgress + " total len:" + len + " progress:" + (firmwareProgress / len)); - if (firmwareProgress >= len) { - builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), new byte[]{MiBandService.COMMAND_SYNC}); - } else { - GB.updateInstallNotification("Firmware write failed", false, 0, getContext()); + if (firmwareProgress < len) { + byte[] lastChunk = Arrays.copyOfRange(fwbytes, packets * packetLength, len); + builder.write(characteristicFWData, lastChunk); + firmwareProgress = len; } + builder.write(characteristicControlPoint, new byte[]{MiBandService.COMMAND_SYNC}); builder.queue(getQueue()); } catch (IOException ex) { LOG.error("Unable to send fw to MI", ex); - GB.updateInstallNotification("Firmware write failed", false, 0, getContext()); + GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_firmware_not_sent), false, 0, getContext()); return false; } return true; } + + private abstract class UpdateCoordinator { + private final boolean reboot; + + public UpdateCoordinator(boolean needsReboot) { + this.reboot = needsReboot; + } + + abstract byte[] getFirmwareInfo(); + + public abstract byte[] getFirmwareBytes(); + + public abstract boolean initNextOperation(); + + public boolean sendFwInfo() { + try { + TransactionBuilder builder = performInitialized("send firmware info"); +// getSupport().setLowLatency(builder); + builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.updating_firmware), getContext())); + builder.add(new FirmwareInfoSentAction()); // Note: *before* actually sending the info, otherwise it's too late! + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), getFirmwareInfo()); + builder.queue(getQueue()); + return true; + } catch (IOException e) { + LOG.error("Error sending firmware info: " + e.getLocalizedMessage(), e); + return false; + } + } + + public boolean sendFwData() { +// if (true) { +// return true; // FIXME: temporarily disabled firmware sending +// } + return sendFirmwareData(getFirmwareBytes()); + } + + public boolean needsReboot() { + return reboot; + } + } + + private class SingleUpdateCoordinator extends UpdateCoordinator { + + private final byte[] fwInfo; + private final byte[] fwData; + + public SingleUpdateCoordinator(byte[] fwInfo, byte[] fwData, boolean reboot) { + super(reboot); + this.fwInfo = fwInfo; + this.fwData = fwData; + } + + @Override + public byte[] getFirmwareInfo() { + return fwInfo; + } + + @Override + public byte[] getFirmwareBytes() { + return fwData; + } + + @Override + public boolean initNextOperation() { + return false; + } + } + + enum State { + INITIAL, + SEND_FW2, + SEND_FW1, + FINISHED, + UNKNOWN + } + + private class DoubleUpdateCoordinator extends UpdateCoordinator { + + private final byte[] fw1Info; + private final byte[] fw1Data; + + private final byte[] fw2Info; + private final byte[] fw2Data; + + private byte[] currentFwInfo; + private byte[] currentFwData; + + private State state = State.INITIAL; + + public DoubleUpdateCoordinator(byte[] fw1Info, byte[] fw1Data, byte[] fw2Info, byte[] fw2Data, boolean reboot) { + super(reboot); + this.fw1Info = fw1Info; + this.fw1Data = fw1Data; + this.fw2Info = fw2Info; + this.fw2Data = fw2Data; + + // start with fw2 (heart rate) + currentFwInfo = fw2Info; + currentFwData = fw2Data; + } + + @Override + public byte[] getFirmwareInfo() { + return currentFwInfo; + } + + @Override + public byte[] getFirmwareBytes() { + return currentFwData; + } + + @Override + public boolean initNextOperation() { + switch (state) { + case INITIAL: + currentFwInfo = fw2Info; + currentFwData = fw2Data; + state = State.SEND_FW2; + return true; + case SEND_FW2: + currentFwInfo = fw1Info; + currentFwData = fw1Data; + state = State.SEND_FW1; + return fw1Info != null && fw1Data != null; + case SEND_FW1: + currentFwInfo = null; + currentFwData = null; + state = State.FINISHED; + return false; // we're done + default: + state = State.UNKNOWN; + } + return false; + } + } + + private class FirmwareInfoSentAction extends PlainAction { + @Override + public boolean run(BluetoothGatt gatt) { + if (isOperationRunning()) { + firmwareInfoSent = true; + } + return true; + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerGBPebble.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerGBPebble.java index 667763d67..f3ac052fa 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerGBPebble.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerGBPebble.java @@ -52,10 +52,10 @@ public class AppMessageHandlerGBPebble extends AppMessageHandler { db = GBApplication.acquireDB(); while (samples_remaining-- > 0) { short sample = samplesBuffer.getShort(); - byte type = (byte) ((sample & 0xe000) >>> 13); - byte intensity = (byte) ((sample & 0x1f80) >>> 7); - byte steps = (byte) (sample & 0x007f); - db.addGBActivitySample(timestamp + offset_seconds, SampleProvider.PROVIDER_PEBBLE_GADGETBRIDGE, (short) (intensity & 0xff), (short) (steps & 0xff), type); + int type = ((sample & 0xe000) >>> 13); + int intensity = ((sample & 0x1f80) >>> 7); + int steps = (sample & 0x007f); + db.addGBActivitySample(timestamp + offset_seconds, SampleProvider.PROVIDER_PEBBLE_GADGETBRIDGE, intensity, steps, type, 0); offset_seconds += 60; } } catch (GBException e) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMisfit.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMisfit.java index 96c53cf4a..daa0b020c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMisfit.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMisfit.java @@ -75,7 +75,7 @@ public class AppMessageHandlerMisfit extends AppMessageHandler { short sample = buf.getShort(); int steps = 0; int intensity = 0; - byte activityKind = ActivityKind.TYPE_UNKNOWN; + int activityKind = ActivityKind.TYPE_UNKNOWN; if (((sample & 0x83ff) == 0x0001) && ((sample & 0xff00) <= 0x4800)) { // sleep seems to be from 0x2401 to 0x4801 (0b0IIIII0000000001) where I = intensity ? @@ -101,7 +101,7 @@ public class AppMessageHandlerMisfit extends AppMessageHandler { totalSteps += steps; LOG.info("got steps for sample " + i + " : " + steps + "(" + Integer.toHexString(sample & 0xffff) + ")"); - activitySamples[i] = new GBActivitySample(sampleProvider, timestamp + i * 60, (short) intensity, (short) steps, activityKind); + activitySamples[i] = new GBActivitySample(sampleProvider, timestamp + i * 60, intensity, steps, activityKind); } LOG.info("total steps for above period: " + totalSteps); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMorpheuz.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMorpheuz.java index 305a4abe0..3c6517d6c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMorpheuz.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMorpheuz.java @@ -80,10 +80,10 @@ public class AppMessageHandlerMorpheuz extends AppMessageHandler { // we have no base timestamp but received points, stop this ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE | AppMessageHandlerMorpheuz.CTRL_GONEOFF_DONE | AppMessageHandlerMorpheuz.CTRL_TRANSMIT_DONE | AppMessageHandlerMorpheuz.CTRL_SET_LAST_SENT; } else { - short index = (short) ((int) pair.second >> 16); - short intensity = (short) ((int) pair.second & 0xffff); + int index = ((int) pair.second >> 16); + int intensity = ((int) pair.second & 0xffff); LOG.info("got point:" + index + " " + intensity); - byte type = MorpheuzSampleProvider.TYPE_UNKNOWN; + int type = MorpheuzSampleProvider.TYPE_UNKNOWN; if (intensity <= 120) { type = MorpheuzSampleProvider.TYPE_DEEP_SLEEP; } else if (intensity <= 1000) { @@ -93,7 +93,7 @@ public class AppMessageHandlerMorpheuz extends AppMessageHandler { DBHandler db = null; try { db = GBApplication.acquireDB(); - db.addGBActivitySample(recording_base_timestamp + index * 600, SampleProvider.PROVIDER_PEBBLE_MORPHEUZ, intensity, (byte) 0, type); + db.addGBActivitySample(recording_base_timestamp + index * 600, SampleProvider.PROVIDER_PEBBLE_MORPHEUZ, intensity, 0, type, 0); } catch (GBException e) { LOG.error("Error acquiring database", e); } finally { @@ -113,12 +113,12 @@ public class AppMessageHandlerMorpheuz extends AppMessageHandler { break; case KEY_TO: smartalarm_to = (int) pair.second; - LOG.info("got from: " + smartalarm_to / 60 + ":" + smartalarm_to % 60); + LOG.info("got to: " + smartalarm_to / 60 + ":" + smartalarm_to % 60); ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE | AppMessageHandlerMorpheuz.CTRL_SET_LAST_SENT | AppMessageHandlerMorpheuz.CTRL_DO_NEXT; break; case KEY_VERSION: LOG.info("got version: " + ((float) ((int) pair.second) / 10.0f)); - ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE | AppMessageHandlerMorpheuz.CTRL_SET_LAST_SENT; + ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE; break; case KEY_BASE: // fix timestamp diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java index 37468e75c..e25a28eac 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java @@ -10,7 +10,6 @@ import java.util.ArrayList; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; -import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleColor; import nodomain.freeyourgadget.gadgetbridge.model.Weather; import ru.gelin.android.weather.notification.ParcelableWeather2; @@ -99,18 +98,24 @@ public class AppMessageHandlerPebStyle extends AppMessageHandler { @Override public GBDeviceEvent[] handleMessage(ArrayList> pairs) { + return null; + /* GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); ByteBuffer buf = ByteBuffer.allocate(encodeAck().length + encodePebStyleConfig().length); buf.put(encodeAck()); buf.put(encodePebStyleConfig()); sendBytes.encodedBytes = buf.array(); return new GBDeviceEvent[]{sendBytes}; + */ } @Override public GBDeviceEvent[] pushMessage() { + return null; + /* GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); sendBytes.encodedBytes = encodePebStyleConfig(); return new GBDeviceEvent[]{sendBytes}; + */ } } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerTimeStylePebble.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerTimeStylePebble.java index da4fc399e..9baeb477d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerTimeStylePebble.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerTimeStylePebble.java @@ -13,6 +13,7 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; import nodomain.freeyourgadget.gadgetbridge.model.Weather; + import ru.gelin.android.weather.notification.ParcelableWeather2; public class AppMessageHandlerTimeStylePebble extends AppMessageHandler { @@ -99,10 +100,13 @@ public class AppMessageHandlerTimeStylePebble extends AppMessageHandler { } - @Override + @Override public GBDeviceEvent[] handleMessage(ArrayList> pairs) { + return null; + /* GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); sendBytes.encodedBytes = encodeTimeStylePebbleConfig(); return new GBDeviceEvent[]{sendBytes}; + */ } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSession.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSession.java new file mode 100644 index 000000000..2b07d944a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSession.java @@ -0,0 +1,29 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; + +import java.nio.ByteBuffer; +import java.util.UUID; + +class DatalogSession { + final byte id; + final int tag; + final UUID uuid; + final byte itemType; + final short itemSize; + String taginfo = "(unknown)"; + + DatalogSession(byte id, UUID uuid, int tag, byte itemType, short itemSize) { + this.id = id; + this.tag = tag; + this.uuid = uuid; + this.itemType = itemType; + this.itemSize = itemSize; + } + + boolean handleMessage(ByteBuffer buf, int length) { + return true; + } + + String getTaginfo() { + return taginfo; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java new file mode 100644 index 000000000..7d06876fd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java @@ -0,0 +1,170 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; + +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.pebble.HealthSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +class DatalogSessionHealthSleep extends DatalogSession { + + private static final Logger LOG = LoggerFactory.getLogger(DatalogSessionHealthSleep.class); + + public DatalogSessionHealthSleep(byte id, UUID uuid, int tag, byte item_type, short item_size) { + super(id, uuid, tag, item_type, item_size); + taginfo = "(health - sleep " + tag + " )"; + } + + @Override + public boolean handleMessage(ByteBuffer datalogMessage, int length) { + LOG.info("DATALOG " + taginfo + GB.hexdump(datalogMessage.array(), datalogMessage.position(), length)); + switch (this.tag) { + case 83: + return handleMessage83(datalogMessage, length); + case 84: + return handleMessage84(datalogMessage, length); + default: + return false; + } + } + + private boolean handleMessage84(ByteBuffer datalogMessage, int length) { + int initialPosition = datalogMessage.position(); + int beginOfRecordPosition; + short recordVersion; //probably + short recordType; //probably: 1=sleep, 2=deep sleep + + if (0 != (length % itemSize)) + return false;//malformed message? + + int recordCount = length / itemSize; + SleepRecord84[] sleepRecords = new SleepRecord84[recordCount]; + + for (int recordIdx = 0; recordIdx < recordCount; recordIdx++) { + beginOfRecordPosition = initialPosition + recordIdx * itemSize; + datalogMessage.position(beginOfRecordPosition);//we may not consume all the bytes of a record + recordVersion = datalogMessage.getShort(); + if (recordVersion != 1) + return false;//we don't know how to deal with the data TODO: this is not ideal because we will get the same message again and again since we NACK it + + datalogMessage.getShort();//throwaway, unknown + recordType = datalogMessage.getShort(); + + sleepRecords[recordIdx] = new SleepRecord84(recordType, datalogMessage.getInt(), datalogMessage.getInt(), datalogMessage.getInt()); + } + + return store84(sleepRecords);//NACK if we cannot store the data yet, the watch will send the sleep records again. + } + + private boolean store84(SleepRecord84[] sleepRecords) { + DBHandler dbHandler = null; + SampleProvider sampleProvider = new HealthSampleProvider(); + try { + dbHandler = GBApplication.acquireDB(); + int latestTimestamp = dbHandler.fetchLatestTimestamp(sampleProvider); + for (SleepRecord84 sleepRecord : sleepRecords) { + if (latestTimestamp < (sleepRecord.timestampStart + sleepRecord.durationSeconds)) + return false; + if (sleepRecord.type == 2) { + dbHandler.changeStoredSamplesType(sleepRecord.timestampStart, (sleepRecord.timestampStart + sleepRecord.durationSeconds), sampleProvider.toRawActivityKind(ActivityKind.TYPE_DEEP_SLEEP), sampleProvider); + } else { + dbHandler.changeStoredSamplesType(sleepRecord.timestampStart, (sleepRecord.timestampStart + sleepRecord.durationSeconds), sampleProvider.toRawActivityKind(ActivityKind.TYPE_ACTIVITY), sampleProvider.toRawActivityKind(ActivityKind.TYPE_LIGHT_SLEEP), sampleProvider); + } + + } + } catch (Exception ex) { + LOG.debug(ex.getMessage()); + } finally { + if (dbHandler != null) { + dbHandler.release(); + } + } + return true; + } + + private boolean handleMessage83(ByteBuffer datalogMessage, int length) { + int initialPosition = datalogMessage.position(); + int beginOfRecordPosition; + short recordVersion; //probably + + if (0 != (length % itemSize)) + return false;//malformed message? + + int recordCount = length / itemSize; + SleepRecord83[] sleepRecords = new SleepRecord83[recordCount]; + + for (int recordIdx = 0; recordIdx < recordCount; recordIdx++) { + beginOfRecordPosition = initialPosition + recordIdx * itemSize; + datalogMessage.position(beginOfRecordPosition);//we may not consume all the bytes of a record + recordVersion = datalogMessage.getShort(); + if (recordVersion != 1) + return false;//we don't know how to deal with the data TODO: this is not ideal because we will get the same message again and again since we NACK it + + sleepRecords[recordIdx] = new SleepRecord83(datalogMessage.getInt(), + datalogMessage.getInt(), + datalogMessage.getInt(), + datalogMessage.getInt()); + } + + return store83(sleepRecords);//NACK if we cannot store the data yet, the watch will send the sleep records again. + } + + private boolean store83(SleepRecord83[] sleepRecords) { + DBHandler dbHandler = null; + SampleProvider sampleProvider = new HealthSampleProvider(); + GB.toast("Deep sleep is supported only from firmware 3.11 onwards.", Toast.LENGTH_LONG, GB.INFO); + try { + dbHandler = GBApplication.acquireDB(); + int latestTimestamp = dbHandler.fetchLatestTimestamp(sampleProvider); + for (SleepRecord83 sleepRecord : sleepRecords) { + if (latestTimestamp < sleepRecord.bedTimeEnd) + return false; + dbHandler.changeStoredSamplesType(sleepRecord.bedTimeStart, sleepRecord.bedTimeEnd, sampleProvider.toRawActivityKind(ActivityKind.TYPE_ACTIVITY), sampleProvider.toRawActivityKind(ActivityKind.TYPE_LIGHT_SLEEP), sampleProvider); + } + } catch (Exception ex) { + LOG.debug(ex.getMessage()); + } finally { + if (dbHandler != null) { + dbHandler.release(); + } + } + return true; + } + + private class SleepRecord83 { + int offsetUTC; //probably + int bedTimeStart; + int bedTimeEnd; + int deepSleepSeconds; + + public SleepRecord83(int offsetUTC, int bedTimeStart, int bedTimeEnd, int deepSleepSeconds) { + this.offsetUTC = offsetUTC; + this.bedTimeStart = bedTimeStart; + this.bedTimeEnd = bedTimeEnd; + this.deepSleepSeconds = deepSleepSeconds; + } + } + + private class SleepRecord84 { + int type; //1=sleep, 2=deep sleep + int offsetUTC; //probably + int timestampStart; + int durationSeconds; + + public SleepRecord84(int type, int offsetUTC, int timestampStart, int durationSeconds) { + this.type = type; + this.offsetUTC = offsetUTC; + this.timestampStart = timestampStart; + this.durationSeconds = durationSeconds; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java new file mode 100644 index 000000000..f89f03639 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java @@ -0,0 +1,115 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.pebble.HealthSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.impl.GBActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class DatalogSessionHealthSteps extends DatalogSession { + + private static final Logger LOG = LoggerFactory.getLogger(DatalogSessionHealthSteps.class); + + public DatalogSessionHealthSteps(byte id, UUID uuid, int tag, byte item_type, short item_size) { + super(id, uuid, tag, item_type, item_size); + taginfo = "(health - steps)"; + } + + @Override + public boolean handleMessage(ByteBuffer datalogMessage, int length) { + LOG.info("DATALOG " + taginfo + GB.hexdump(datalogMessage.array(), datalogMessage.position(), length)); + + int timestamp; + byte recordLength, recordNum; + short recordVersion; //probably + int beginOfPacketPosition, beginOfRecordPosition; + + int initialPosition = datalogMessage.position(); + if (0 != (length % itemSize)) + return false;//malformed message? + + int packetCount = length / itemSize; + + for (int packetIdx = 0; packetIdx < packetCount; packetIdx++) { + beginOfPacketPosition = initialPosition + packetIdx * itemSize; + datalogMessage.position(beginOfPacketPosition);//we may not consume all the records of a packet + + recordVersion = datalogMessage.getShort(); + + if ((recordVersion != 5) && (recordVersion != 6)) + return false; //we don't know how to deal with the data TODO: this is not ideal because we will get the same message again and again since we NACK it + + timestamp = datalogMessage.getInt(); + datalogMessage.get(); //unknown, throw away + recordLength = datalogMessage.get(); + recordNum = datalogMessage.get(); + + beginOfRecordPosition = datalogMessage.position(); + StepsRecord[] stepsRecords = new StepsRecord[recordNum]; + + for (int recordIdx = 0; recordIdx < recordNum; recordIdx++) { + datalogMessage.position(beginOfRecordPosition + recordIdx * recordLength); //we may not consume all the bytes of a record + stepsRecords[recordIdx] = new StepsRecord(timestamp, datalogMessage.get() & 0xff, datalogMessage.get() & 0xff, datalogMessage.getShort() & 0xffff, datalogMessage.get() & 0xff); + timestamp += 60; + } + + store(stepsRecords); + } + return true;//ACK by default + } + + private void store(StepsRecord[] stepsRecords) { + + DBHandler dbHandler = null; + SampleProvider sampleProvider = new HealthSampleProvider(); + + ActivitySample[] samples = new ActivitySample[stepsRecords.length]; + for (int j = 0; j < stepsRecords.length; j++) { + StepsRecord stepsRecord = stepsRecords[j]; + samples[j] = new GBActivitySample( + sampleProvider, + stepsRecord.timestamp, + stepsRecord.intensity, + stepsRecord.steps, + sampleProvider.toRawActivityKind(ActivityKind.TYPE_ACTIVITY)); + } + + try { + dbHandler = GBApplication.acquireDB(); + dbHandler.addGBActivitySamples(samples); + } catch (Exception ex) { + LOG.debug(ex.getMessage()); + } finally { + if (dbHandler != null) { + dbHandler.release(); + } + } + } + + private class StepsRecord { + int timestamp; + int steps; + int orientation; + int intensity; + int light_intensity; + + public StepsRecord(int timestamp, int steps, int orientation, int intensity, int light_intensity) { + this.timestamp = timestamp; + this.steps = steps; + this.orientation = orientation; + this.intensity = intensity; + this.light_intensity = light_intensity; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java index 49165df49..43b86e2f1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java @@ -8,10 +8,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.net.Uri; import android.os.ParcelUuid; -import android.preference.PreferenceManager; import org.json.JSONArray; import org.json.JSONException; @@ -29,6 +27,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; @@ -43,6 +42,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class PebbleIoThread extends GBDeviceIoThread { private static final Logger LOG = LoggerFactory.getLogger(PebbleIoThread.class); @@ -61,7 +62,7 @@ public class PebbleIoThread extends GBDeviceIoThread { public static final String PEBBLEKIT_ACTION_APP_START = "com.getpebble.action.app.START"; public static final String PEBBLEKIT_ACTION_APP_STOP = "com.getpebble.action.app.STOP"; - final SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + final Prefs prefs = GBApplication.getPrefs(); private final PebbleProtocol mPebbleProtocol; private final PebbleSupport mPebbleSupport; @@ -159,7 +160,7 @@ public class PebbleIoThread extends GBDeviceIoThread { mPebbleProtocol = (PebbleProtocol) gbDeviceProtocol; mBtAdapter = btAdapter; mPebbleSupport = pebbleSupport; - mEnablePebblekit = sharedPrefs.getBoolean("pebble_enable_pebblekit", false); + mEnablePebblekit = prefs.getBoolean("pebble_enable_pebblekit", false); } @Override @@ -198,12 +199,17 @@ public class PebbleIoThread extends GBDeviceIoThread { return false; } - mPebbleProtocol.setForceProtocol(sharedPrefs.getBoolean("pebble_force_protocol", false)); - gbDevice.setState(GBDevice.State.CONNECTED); - gbDevice.sendDeviceUpdateIntent(getContext()); + mPebbleProtocol.setForceProtocol(prefs.getBoolean("pebble_force_protocol", false)); mIsConnected = true; - write(mPebbleProtocol.encodeFirmwareVersionReq()); + if (originalState == GBDevice.State.WAITING_FOR_RECONNECT) { + gbDevice.setState(GBDevice.State.INITIALIZED); + } else { + gbDevice.setState(GBDevice.State.CONNECTED); + write(mPebbleProtocol.encodeFirmwareVersionReq()); + } + gbDevice.sendDeviceUpdateIntent(getContext()); + return true; } @@ -358,13 +364,23 @@ public class PebbleIoThread extends GBDeviceIoThread { if (e.getMessage().contains("socket closed")) { //FIXME: this does not feel right LOG.info(e.getMessage()); mIsConnected = false; - int reconnectAttempts = Integer.valueOf(sharedPrefs.getString("pebble_reconnect_attempts", "10")); - if (reconnectAttempts > 0) { + int reconnectAttempts = prefs.getInt("pebble_reconnect_attempts", 10); + if (GBApplication.getGBPrefs().getAutoReconnect() && reconnectAttempts > 0) { gbDevice.setState(GBDevice.State.CONNECTING); gbDevice.sendDeviceUpdateIntent(getContext()); - while (reconnectAttempts-- > 0 && !mQuit) { + int delaySeconds = 1; + while (reconnectAttempts-- > 0 && !mQuit && !mIsConnected) { LOG.info("Trying to reconnect (attempts left " + reconnectAttempts + ")"); mIsConnected = connect(gbDevice.getAddress()); + if (!mIsConnected) { + try { + Thread.sleep(delaySeconds * 1000); + } catch (InterruptedException ignored) { + } + if (delaySeconds < 64) { + delaySeconds *= 2; + } + } } } if (!mIsConnected && !mQuit) { @@ -380,6 +396,14 @@ public class PebbleIoThread extends GBDeviceIoThread { } catch (IOException ex) { ex.printStackTrace(); LOG.info("error while reconnecting"); + } finally { + try { + if (mBtServerSocket != null) { + mBtServerSocket.close(); + mBtServerSocket = null; + } + } catch (IOException ignore) { + } } } if (!mIsConnected) { @@ -464,10 +488,11 @@ public class PebbleIoThread extends GBDeviceIoThread { private boolean evaluateGBDeviceEventPebble(GBDeviceEvent deviceEvent) { if (deviceEvent instanceof GBDeviceEventVersionInfo) { - if (sharedPrefs.getBoolean("datetime_synconconnect", true)) { + if (prefs.getBoolean("datetime_synconconnect", true)) { LOG.info("syncing time"); write(mPebbleProtocol.encodeSetTime()); } + write(mPebbleProtocol.encodeReportDataLogSessions()); gbDevice.setState(GBDevice.State.INITIALIZED); return false; } else if (deviceEvent instanceof GBDeviceEventAppManagement) { @@ -572,18 +597,11 @@ public class PebbleIoThread extends GBDeviceIoThread { if (uri.equals(Uri.parse("fake://health"))) { write(mPebbleProtocol.encodeActivateHealth(true)); + write(mPebbleProtocol.encodeSetSaneDistanceUnit(true)); return; } - String hwRev = gbDevice.getHardwareVersion(); - String platformName; - if (hwRev.startsWith("snowy")) { - platformName = "basalt"; - } else if (hwRev.startsWith("spalding")) { - platformName = "chalk"; - } else { - platformName = "aplite"; - } + String platformName = PebbleUtils.getPlatformName(gbDevice.getHardwareVersion()); try { mPBWReader = new PBWReader(uri, getContext(), platformName); @@ -618,7 +636,7 @@ public class PebbleIoThread extends GBDeviceIoThread { if (appId == 0) { // only install metadata - not the binaries write(mPebbleProtocol.encodeInstallMetadata(app.getUUID(), app.getName(), mPBWReader.getAppVersion(), mPBWReader.getSdkVersion(), mPBWReader.getFlags(), mPBWReader.getIconId())); - GB.toast("To finish installation please start the watchapp on your Pebble", 5, GB.INFO); + write(mPebbleProtocol.encodeAppStart(app.getUUID(), true)); } else { // this came from an app fetch request, so do the real stuff mIsInstalling = true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index fc1091930..ad947bcf3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -32,9 +32,10 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInf import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleColor; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleIconID; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; -import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; public class PebbleProtocol extends GBDeviceProtocol { @@ -81,7 +82,7 @@ public class PebbleProtocol extends GBDeviceProtocol { static final byte BLOBDB_APP = 2; static final byte BLOBDB_REMINDER = 3; static final byte BLOBDB_NOTIFICATION = 4; - static final byte BLOBDB_HEALTH = 7; // might also be some generic registry database + static final byte BLOBDB_PREFERENCES = 7; static final byte BLOBDB_SUCCESS = 1; static final byte BLOBDB_GENERALFAILURE = 2; static final byte BLOBDB_INVALIDOPERATION = 3; @@ -110,7 +111,9 @@ public class PebbleProtocol extends GBDeviceProtocol { static final byte PHONECONTROL_START = 8; static final byte PHONECONTROL_END = 9; - static final byte MUSICCONTROL_SETMUSICINFO = 16; + static final byte MUSICCONTROL_SETMUSICINFO = 0x10; + static final byte MUSICCONTROL_SETPLAYSTATE = 0x11; + static final byte MUSICCONTROL_PLAYPAUSE = 1; static final byte MUSICCONTROL_PAUSE = 2; static final byte MUSICCONTROL_PLAY = 3; @@ -118,7 +121,13 @@ public class PebbleProtocol extends GBDeviceProtocol { static final byte MUSICCONTROL_PREVIOUS = 5; static final byte MUSICCONTROL_VOLUMEUP = 6; static final byte MUSICCONTROL_VOLUMEDOWN = 7; - static final byte MUSICCONTROL_GETNOWPLAYING = 7; + static final byte MUSICCONTROL_GETNOWPLAYING = 8; + + static final byte MUSICCONTROL_STATE_PAUSED = 0x00; + static final byte MUSICCONTROL_STATE_PLAYING = 0x01; + static final byte MUSICCONTROL_STATE_REWINDING = 0x02; + static final byte MUSICCONTROL_STATE_FASTWORWARDING = 0x03; + static final byte MUSICCONTROL_STATE_UNKNOWN = 0x04; static final byte NOTIFICATIONACTION_ACK = 0; static final byte NOTIFICATIONACTION_NACK = 1; @@ -352,6 +361,8 @@ public class PebbleProtocol extends GBDeviceProtocol { private static final UUID UUID_PEBSTYLE = UUID.fromString("da05e84d-e2a2-4020-a2dc-9cdcf265fcdd"); private static final UUID UUID_MARIOTIME = UUID.fromString("43caa750-2896-4f46-94dc-1adbd4bc1ff3"); + private static final UUID UUID_ZERO = new UUID(0, 0); + private static final Map mAppMessageHandlers = new HashMap<>(); { @@ -364,6 +375,8 @@ public class PebbleProtocol extends GBDeviceProtocol { mAppMessageHandlers.put(UUID_MARIOTIME, new AppMessageHandlerMarioTime(UUID_MARIOTIME, PebbleProtocol.this)); } + private final HashMap mDatalogSessions = new HashMap<>(); + private static byte[] encodeSimpleMessage(short endpoint, byte command) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_SIMPLEMESSAGE); buf.order(ByteOrder.BIG_ENDIAN); @@ -374,7 +387,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - private static byte[] encodeMessage(short endpoint, byte type, int cookie, String[] parts) { + private byte[] encodeMessage(short endpoint, byte type, int cookie, String[] parts) { // Calculate length first int length = LENGTH_PREFIX + 1; if (parts != null) { @@ -404,7 +417,6 @@ public class PebbleProtocol extends GBDeviceProtocol { if (parts != null) { for (String s : parts) { if (s == null || s.equals("")) { - //buf.put((byte)0x01); buf.put((byte) 0x00); continue; } @@ -485,7 +497,7 @@ public class PebbleProtocol extends GBDeviceProtocol { @Override public byte[] encodeFindDevice(boolean start) { - return encodeSetCallState("Where are you?", "Gadgetbridge", start ? ServiceCommand.CALL_INCOMING : ServiceCommand.CALL_END); + return encodeSetCallState("Where are you?", "Gadgetbridge", start ? CallSpec.CALL_INCOMING : CallSpec.CALL_END); } private static byte[] encodeExtensibleNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName, boolean hasHandle, String[] cannedReplies) { @@ -681,11 +693,39 @@ public class PebbleProtocol extends GBDeviceProtocol { byte command; command = BLOBDB_INSERT; if (activate) { - blob = new byte[]{0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}; + + ByteBuffer buf = ByteBuffer.allocate(9); + buf.order(ByteOrder.LITTLE_ENDIAN); + + ActivityUser activityUser = new ActivityUser(); + Integer heightMm = activityUser.getActivityUserHeightCm() * 10; + buf.putShort(heightMm.shortValue()); + Integer weigthDag = activityUser.getActivityUserWeightKg() * 100; + buf.putShort(weigthDag.shortValue()); + buf.put((byte) 0x01); //activate tracking + buf.put((byte) 0x00); //activity Insights + buf.put((byte) 0x00); //sleep Insights + buf.put((byte) activityUser.getActivityUserAge()); + buf.put((byte) activityUser.getActivityUserGender()); + blob = buf.array(); } else { blob = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; } - return encodeBlobdb("activityPreferences", command, BLOBDB_HEALTH, blob); + return encodeBlobdb("activityPreferences", command, BLOBDB_PREFERENCES, blob); + } + + public byte[] encodeSetSaneDistanceUnit(boolean sane) { + byte value; + if (sane) { + value = 0x00; + } else { + value = 0x01; + } + return encodeBlobdb("unitsDistance", BLOBDB_INSERT, BLOBDB_PREFERENCES, new byte[]{value}); + } + + public byte[] encodeReportDataLogSessions() { + return encodeSimpleMessage(ENDPOINT_DATALOG, DATALOG_REPORTSESSIONS); } private byte[] encodeBlobDBClear(byte database) { @@ -771,33 +811,21 @@ public class PebbleProtocol extends GBDeviceProtocol { icon_id = PebbleIconID.GENERIC_SMS; color_id = PebbleColor.VividViolet; break; + case TWITTER: + icon_id = PebbleIconID.NOTIFICATION_TWITTER; + color_id = PebbleColor.BlueMoon; + break; + case FACEBOOK: + icon_id = PebbleIconID.NOTIFICATION_FACEBOOK; + color_id = PebbleColor.VeryLightBlue; + break; + case CHAT: + icon_id = PebbleIconID.NOTIFICATION_HIPCHAT; + color_id = PebbleColor.Inchworm; + break; default: - switch (notificationType) { - case TWITTER: - icon_id = PebbleIconID.NOTIFICATION_TWITTER; - color_id = PebbleColor.BlueMoon; - break; - case EMAIL: - icon_id = PebbleIconID.GENERIC_EMAIL; - color_id = PebbleColor.JaegerGreen; - break; - case SMS: - icon_id = PebbleIconID.GENERIC_SMS; - color_id = PebbleColor.VividViolet; - break; - case FACEBOOK: - icon_id = PebbleIconID.NOTIFICATION_FACEBOOK; - color_id = PebbleColor.VeryLightBlue; - break; - case CHAT: - icon_id = PebbleIconID.NOTIFICATION_HIPCHAT; - color_id = PebbleColor.Inchworm; - break; - default: - icon_id = PebbleIconID.NOTIFICATION_GENERIC; - color_id = PebbleColor.Red; - break; - } + icon_id = PebbleIconID.NOTIFICATION_GENERIC; + color_id = PebbleColor.Red; break; } // Calculate length first @@ -1018,20 +1046,20 @@ public class PebbleProtocol extends GBDeviceProtocol { } @Override - public byte[] encodeSetCallState(String number, String name, ServiceCommand command) { + public byte[] encodeSetCallState(String number, String name, int command) { String[] parts = {number, name}; byte pebbleCmd; switch (command) { - case CALL_START: + case CallSpec.CALL_START: pebbleCmd = PHONECONTROL_START; break; - case CALL_END: + case CallSpec.CALL_END: pebbleCmd = PHONECONTROL_END; break; - case CALL_INCOMING: + case CallSpec.CALL_INCOMING: pebbleCmd = PHONECONTROL_INCOMINGCALL; break; - case CALL_OUTGOING: + case CallSpec.CALL_OUTGOING: // pebbleCmd = PHONECONTROL_OUTGOINGCALL; /* * HACK/WORKAROUND for non-working outgoing call display. @@ -1051,10 +1079,73 @@ public class PebbleProtocol extends GBDeviceProtocol { return encodeMessage(ENDPOINT_PHONECONTROL, pebbleCmd, 0, parts); } + public byte[] encodeSetMusicState(byte state, int position, int playRate, byte shuffle, byte repeat) { + int length = LENGTH_PREFIX + 12; + // Encode Prefix + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.BIG_ENDIAN); + buf.putShort((short) (length - LENGTH_PREFIX)); + buf.putShort(ENDPOINT_MUSICCONTROL); + + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put(MUSICCONTROL_SETPLAYSTATE); + buf.put(state); + buf.putInt(position); + buf.putInt(playRate); + buf.put(shuffle); + buf.put(repeat); + + return buf.array(); + } + @Override - public byte[] encodeSetMusicInfo(String artist, String album, String track) { + public byte[] encodeSetMusicInfo(String artist, String album, String track, int duration, int trackCount, int trackNr) { String[] parts = {artist, album, track}; - return encodeMessage(ENDPOINT_MUSICCONTROL, MUSICCONTROL_SETMUSICINFO, 0, parts); + if (duration == 0) { + return encodeMessage(ENDPOINT_MUSICCONTROL, MUSICCONTROL_SETMUSICINFO, 0, parts); + } else { + byte[] stateMessage = encodeSetMusicState(MUSICCONTROL_STATE_PLAYING, 0, 100, (byte) 1, (byte) 1); + // Calculate length first + int length = LENGTH_PREFIX + 9; + if (parts != null) { + for (String s : parts) { + if (s == null || s.equals("")) { + length++; // encode null or empty strings as 0x00 later + continue; + } + length += (1 + s.getBytes().length); + } + } + + // Encode Prefix + ByteBuffer buf = ByteBuffer.allocate(length + stateMessage.length); + buf.order(ByteOrder.BIG_ENDIAN); + buf.putShort((short) (length - LENGTH_PREFIX)); + buf.putShort(ENDPOINT_MUSICCONTROL); + buf.put(MUSICCONTROL_SETMUSICINFO); + + // Encode Pascal-Style Strings + for (String s : parts) { + if (s == null || s.equals("")) { + buf.put((byte) 0x00); + continue; + } + + int partlength = s.getBytes().length; + if (partlength > 255) partlength = 255; + buf.put((byte) partlength); + buf.put(s.getBytes(), 0, partlength); + } + + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(duration * 1000); + buf.putShort((short) (trackCount & 0xffff)); + buf.putShort((short) (trackNr & 0xffff)); + + buf.put(stateMessage); + + return buf.array(); + } } @Override @@ -1629,11 +1720,11 @@ public class PebbleProtocol extends GBDeviceProtocol { // FIXME: this does not belong here, but we want at least check if there is no chance at all to send out the SMS later before we report success String phoneNumber = (String) GBApplication.getIDSenderLookup().lookup(id); //if (phoneNumber != null) { - devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY; - devEvtNotificationControl.reply = new String(reply); - caption = "SENT"; - icon_id = PebbleIconID.RESULT_SENT; - failed = false; + devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY; + devEvtNotificationControl.reply = new String(reply); + caption = "SENT"; + icon_id = PebbleIconID.RESULT_SENT; + failed = false; //} } } @@ -1691,7 +1782,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return null; } - private GBDeviceEvent decodeAppRunState(ByteBuffer buf) { + private GBDeviceEvent[] decodeAppRunState(ByteBuffer buf) { byte command = buf.get(); long uuid_high = buf.getLong(); long uuid_low = buf.getLong(); @@ -1700,9 +1791,10 @@ public class PebbleProtocol extends GBDeviceProtocol { switch (command) { case APPRUNSTATE_START: LOG.info(ENDPOINT_NAME + ": started " + uuid); - if (UUID_PEBSTYLE.equals(uuid)) { - AppMessageHandler handler = mAppMessageHandlers.get(uuid); - return handler.pushMessage()[0]; + + AppMessageHandler handler = mAppMessageHandlers.get(uuid); + if (handler != null) { + return handler.pushMessage(); } break; case APPRUNSTATE_STOP: @@ -1759,6 +1851,7 @@ public class PebbleProtocol extends GBDeviceProtocol { } private GBDeviceEventSendBytes decodeDatalog(ByteBuffer buf, short length) { + boolean ack = true; byte command = buf.get(); byte id = buf.get(); switch (command) { @@ -1769,7 +1862,12 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.order(ByteOrder.LITTLE_ENDIAN); int items_left = buf.getInt(); int crc = buf.getInt(); - LOG.info("DATALOG SENDDATA. id=" + (id & 0xff) + ", items_left=" + items_left + ", total length=" + (length - 9)); + DatalogSession datalogSession = mDatalogSessions.get(id); + LOG.info("DATALOG SENDDATA. id=" + (id & 0xff) + ", items_left=" + items_left + ", total length=" + (length - 10)); + if (datalogSession != null) { + LOG.info("DATALOG UUID=" + datalogSession.uuid + ", tag=" + datalogSession.tag + datalogSession.getTaginfo() + ", itemSize=" + datalogSession.itemSize + ", itemType=" + datalogSession.itemType); + ack = datalogSession.handleMessage(buf, length - 10); + } break; case DATALOG_OPENSESSION: buf.order(ByteOrder.BIG_ENDIAN); @@ -1780,16 +1878,36 @@ public class PebbleProtocol extends GBDeviceProtocol { int timestamp = buf.getInt(); int log_tag = buf.getInt(); byte item_type = buf.get(); - short item_size = buf.get(); - LOG.info("DATALOG OPENSESSION. id=" + (id & 0xff) + ", App UUID=" + uuid.toString() + ", item_type=" + item_type + ", item_size=" + item_size); + short item_size = buf.getShort(); + LOG.info("DATALOG OPENSESSION. id=" + (id & 0xff) + ", App UUID=" + uuid.toString() + ", log_tag=" + log_tag + ", item_type=" + item_type + ", itemSize=" + item_size); + if (!mDatalogSessions.containsKey(id)) { + if (uuid.equals(UUID_ZERO) && log_tag == 81) { + mDatalogSessions.put(id, new DatalogSessionHealthSteps(id, uuid, log_tag, item_type, item_size)); + } else if (uuid.equals(UUID_ZERO) && (log_tag == 83 || log_tag == 84)) { + mDatalogSessions.put(id, new DatalogSessionHealthSleep(id, uuid, log_tag, item_type, item_size)); + } else { + mDatalogSessions.put(id, new DatalogSession(id, uuid, log_tag, item_type, item_size)); + } + } + break; + case DATALOG_CLOSE: + LOG.info("DATALOG_CLOSE. id=" + (id & 0xff)); + if (mDatalogSessions.containsKey(id)) { + mDatalogSessions.remove(id); + } break; default: LOG.info("unknown DATALOG command: " + (command & 0xff)); break; } - LOG.info("sending ACK (0x85)"); GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); - sendBytes.encodedBytes = encodeDatalog(id, DATALOG_ACK); + if (ack) { + LOG.info("sending ACK (0x85)"); + sendBytes.encodedBytes = encodeDatalog(id, DATALOG_ACK); + } else { + LOG.info("sending NACK (0x86)"); + sendBytes.encodedBytes = encodeDatalog(id, DATALOG_NACK); + } return sendBytes; } @@ -2034,7 +2152,7 @@ public class PebbleProtocol extends GBDeviceProtocol { devEvts = new GBDeviceEvent[]{decodeSystemMessage(buf)}; break; case ENDPOINT_APPRUNSTATE: - devEvts = new GBDeviceEvent[]{decodeAppRunState(buf)}; + devEvts = decodeAppRunState(buf); break; case ENDPOINT_BLOBDB: devEvts = new GBDeviceEvent[]{decodeBlobDb(buf)}; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java index 590bbb1df..74e4736d9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java @@ -1,10 +1,20 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; import android.net.Uri; +import android.util.Pair; + +import org.json.JSONException; +import org.json.JSONObject; import java.util.ArrayList; +import java.util.Iterator; +import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +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; @@ -29,7 +39,7 @@ public class PebbleSupport extends AbstractSerialDeviceSupport { @Override public boolean useAutoConnect() { - return false; + return true; } @Override @@ -37,11 +47,73 @@ public class PebbleSupport extends AbstractSerialDeviceSupport { getDeviceIOThread().installApp(uri, 0); } + @Override + public void onAppConfiguration(UUID uuid, String config) { + try { + ArrayList> pairs = new ArrayList<>(); + + JSONObject json = new JSONObject(config); + Iterator keysIterator = json.keys(); + while (keysIterator.hasNext()) { + String keyStr = keysIterator.next(); + Object object = json.get(keyStr); + pairs.add(new Pair<>(Integer.parseInt(keyStr), object)); + } + getDeviceIOThread().write(((PebbleProtocol) getDeviceProtocol()).encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, uuid, pairs)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @Override + public void onHeartRateTest() { + + } + @Override public synchronized PebbleIoThread getDeviceIOThread() { return (PebbleIoThread) super.getDeviceIOThread(); } + private boolean reconnect() { + if (!isConnected() && useAutoConnect()) { + if (getDevice().getState() == GBDevice.State.WAITING_FOR_RECONNECT) { + gbDeviceIOThread.interrupt(); + gbDeviceIOThread = null; + if (!connect()) { + return false; + } + try { + Thread.sleep(4000); // this is about the time the connect takes, so the notification can come though + } catch (InterruptedException ignored) { + } + } + } + return true; + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + if (reconnect()) { + super.onNotification(notificationSpec); + } + } + + @Override + public void onSetCallState(CallSpec callSpec) { + if (reconnect()) { + super.onSetCallState(callSpec); + } + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + if (reconnect()) { + super.onSetMusicInfo(musicSpec); + } + } + + @Override public void onSetAlarms(ArrayList alarms) { //nothing to do ATM diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/receivers/GBMusicControlReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/receivers/GBMusicControlReceiver.java index fce017656..459d3c15d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/receivers/GBMusicControlReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/receivers/GBMusicControlReceiver.java @@ -3,16 +3,16 @@ package nodomain.freeyourgadget.gadgetbridge.service.receivers; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.media.AudioManager; import android.os.SystemClock; -import android.preference.PreferenceManager; import android.view.KeyEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class GBMusicControlReceiver extends BroadcastReceiver { private static final Logger LOG = LoggerFactory.getLogger(GBMusicControlReceiver.class); @@ -53,8 +53,8 @@ public class GBMusicControlReceiver extends BroadcastReceiver { } if (keyCode != -1) { - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); - String audioPlayer = sharedPrefs.getString("audio_player", "default"); + Prefs prefs = GBApplication.getPrefs(); + String audioPlayer = prefs.getString("audio_player", "default"); long eventtime = SystemClock.uptimeMillis(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java index eaea3c84d..5dfa8b2a5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java @@ -8,8 +8,9 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; -import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; /** @@ -29,8 +30,8 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport private static final Logger LOG = LoggerFactory.getLogger(AbstractDeviceSupport.class); - private GBDeviceProtocol gbDeviceProtocol; - private GBDeviceIoThread gbDeviceIOThread; + protected GBDeviceProtocol gbDeviceProtocol; + protected GBDeviceIoThread gbDeviceIOThread; /** * Factory method to create the device specific GBDeviceProtocol instance to be used. @@ -47,11 +48,7 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport // currently only one thread allowed if (gbDeviceIOThread != null) { gbDeviceIOThread.quit(); - try { - gbDeviceIOThread.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } + gbDeviceIOThread.interrupt(); gbDeviceIOThread = null; } } @@ -120,14 +117,14 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport } @Override - public void onSetCallState(String number, String name, ServiceCommand command) { - byte[] bytes = gbDeviceProtocol.encodeSetCallState(number, name, command); + public void onSetCallState(CallSpec callSpec) { + byte[] bytes = gbDeviceProtocol.encodeSetCallState(callSpec.number, callSpec.name, callSpec.command); sendToDevice(bytes); } @Override - public void onSetMusicInfo(String artist, String album, String track) { - byte[] bytes = gbDeviceProtocol.encodeSetMusicInfo(artist, album, track); + public void onSetMusicInfo(MusicSpec musicSpec) { + byte[] bytes = gbDeviceProtocol.encodeSetMusicInfo(musicSpec.artist, musicSpec.album, musicSpec.track, musicSpec.duration, musicSpec.trackCount, musicSpec.trackNr); sendToDevice(bytes); } @@ -178,4 +175,16 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport byte[] bytes = gbDeviceProtocol.encodeEnableRealtimeSteps(enable); sendToDevice(bytes); } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + byte[] bytes = gbDeviceProtocol.encodeEnableHeartRateSleepSupport(enable); + sendToDevice(bytes); + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + byte[] bytes = gbDeviceProtocol.encodeEnableRealtimeHeartRateMeasurement(enable); + sendToDevice(bytes); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java index 439c75290..bdf1b1014 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java @@ -4,7 +4,6 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; -import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand; public abstract class GBDeviceProtocol { @@ -16,11 +15,11 @@ public abstract class GBDeviceProtocol { return null; } - public byte[] encodeSetCallState(String number, String name, ServiceCommand command) { + public byte[] encodeSetCallState(String number, String name, int command) { return null; } - public byte[] encodeSetMusicInfo(String artist, String album, String track) { + public byte[] encodeSetMusicInfo(String artist, String album, String track, int duration, int trackCount, int trackNr) { return null; } @@ -60,6 +59,12 @@ public abstract class GBDeviceProtocol { return null; } + public byte[] encodeEnableHeartRateSleepSupport(boolean enable) { + return null; + } + + public byte[] encodeEnableRealtimeHeartRateMeasurement(boolean enable) { return null; } + public GBDeviceEvent[] decodeResponse(byte[] responseData) { return null; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java new file mode 100644 index 000000000..7c767cc11 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java @@ -0,0 +1,39 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +public class ArrayUtils { + /** + * Checks the two given arrays for equality, but comparing only a subset of the second + * array with the whole first array. + * + * @param first the whole array to compare against + * @param second the array, of which a subset shall be compared against the whole first array + * @param secondStartIndex the start index (inclusive) of the second array from which to start the comparison + * @param secondEndIndex the end index (exclusive) of the second array until which to compare + * @return whether the first byte array is equal to the specified subset of the second byte array + * @throws IllegalArgumentException when one of the arrays is null or start and end index are wrong + */ + public static boolean equals(byte[] first, byte[] second, int secondStartIndex, int secondEndIndex) { + if (first == null) { + throw new IllegalArgumentException("first must not be null"); + } + if (second == null) { + throw new IllegalArgumentException("second must not be null"); + } + if (secondStartIndex >= secondEndIndex) { + throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex"); + } + if (second.length < secondEndIndex) { + throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex"); + } + if (first.length < secondEndIndex) { + return false; + } + int len = secondEndIndex - secondStartIndex; + for (int i = 0; i < len; i++) { + if (first[i] != second[secondStartIndex + i]) { + return false; + } + } + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java index f7ac4e159..ef5078760 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java @@ -49,4 +49,10 @@ public class DateTimeUtils { Date newDate = cal.getTime(); return newDate; } + + public static Date parseTimeStamp(int timestamp) { + GregorianCalendar cal = (GregorianCalendar) GregorianCalendar.getInstance(); + cal.setTimeInMillis(timestamp * 1000L); // make sure it's converted to long + return cal.getTime(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index ae7ee58ed..ee274dc7b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -3,16 +3,19 @@ package nodomain.freeyourgadget.gadgetbridge.util; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; import android.widget.Toast; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator; @@ -24,6 +27,9 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; public class DeviceHelper { + + private static final Logger LOG = LoggerFactory.getLogger(DeviceHelper.class); + private static final DeviceHelper instance = new DeviceHelper(); public static DeviceHelper getInstance() { @@ -76,8 +82,8 @@ public class DeviceHelper { } } - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); - String miAddr = sharedPrefs.getString(MiBandConst.PREF_MIBAND_ADDRESS, ""); + Prefs prefs = GBApplication.getPrefs(); + String miAddr = prefs.getString(MiBandConst.PREF_MIBAND_ADDRESS, ""); if (miAddr.length() > 0) { GBDevice miDevice = new GBDevice(miAddr, "MI", DeviceType.MIBAND); if (!availableDevices.contains(miDevice)) { @@ -85,8 +91,8 @@ public class DeviceHelper { } } - String pebbleEmuAddr = sharedPrefs.getString("pebble_emu_addr", ""); - String pebbleEmuPort = sharedPrefs.getString("pebble_emu_port", ""); + String pebbleEmuAddr = prefs.getString("pebble_emu_addr", ""); + String pebbleEmuPort = prefs.getString("pebble_emu_port", ""); if (pebbleEmuAddr.length() >= 7 && pebbleEmuPort.length() > 0) { GBDevice pebbleEmuDevice = new GBDevice(pebbleEmuAddr + ":" + pebbleEmuPort, "Pebble qemu", DeviceType.PEBBLE); availableDevices.add(pebbleEmuDevice); @@ -97,12 +103,23 @@ public class DeviceHelper { public GBDevice toSupportedDevice(BluetoothDevice device) { GBDeviceCandidate candidate = new GBDeviceCandidate(device, GBDevice.RSSI_UNKNOWN); + + String deviceName = device.getName(); + try { + Method method = device.getClass().getMethod("getAliasName"); + if (method != null) { + deviceName = (String) method.invoke(device); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignore) { + LOG.info("Could not get device alias for " + deviceName); + } + if (coordinator != null && coordinator.supports(candidate)) { - return new GBDevice(device.getAddress(), device.getName(), coordinator.getDeviceType()); + return new GBDevice(device.getAddress(), deviceName, coordinator.getDeviceType()); } for (DeviceCoordinator coordinator : getAllCoordinators()) { if (coordinator.supports(candidate)) { - return new GBDevice(device.getAddress(), device.getName(), coordinator.getDeviceType()); + return new GBDevice(device.getAddress(), deviceName, coordinator.getDeviceType()); } } return null; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java index de9bc9dcd..b354edb74 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java @@ -47,6 +47,16 @@ public class FileUtils { } } + public static void copyStreamToFile(InputStream inputStream, File destFile) throws IOException { + FileOutputStream fout = new FileOutputStream(destFile); + byte[] buf = new byte[4096]; + while (inputStream.available() > 0) { + int bytes = inputStream.read(buf); + fout.write(buf, 0, bytes); + } + fout.close(); + } + public static void copyURItoFile(Context ctx, Uri uri, File destFile) throws IOException { if (uri.getPath().equals(destFile.getPath())) { return; @@ -60,14 +70,8 @@ public class FileUtils { e.printStackTrace(); return; } - FileOutputStream fout = new FileOutputStream(destFile); - byte[] buf = new byte[4096]; - while (fin.available() > 0) { - int bytes = fin.read(buf); - fout.write(buf, 0, bytes); - } + copyStreamToFile(fin, destFile); fin.close(); - fout.close(); } public static String getStringFromFile(File file) throws IOException { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java index 7571badc7..07a7d5939 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java @@ -39,9 +39,13 @@ public class GB { public static final int INFO = 1; public static final int WARN = 2; public static final int ERROR = 3; + public static final String ACTION_DISPLAY_MESSAGE = "GB_Display_Message"; + public static final String DISPLAY_MESSAGE_MESSAGE = "message"; + public static final String DISPLAY_MESSAGE_DURATION = "duration"; + public static final String DISPLAY_MESSAGE_SEVERITY = "severity"; public static GBEnvironment environment; - public static Notification createNotification(String text, Context context) { + public static Notification createNotification(String text, boolean connected, Context context) { if (env().isLocalTest()) { return null; } @@ -55,7 +59,7 @@ public class GB { builder.setContentTitle(context.getString(R.string.app_name)) .setTicker(text) .setContentText(text) - .setSmallIcon(R.drawable.ic_notification) + .setSmallIcon(connected ? R.drawable.ic_notification : R.drawable.ic_notification_disconnected) .setContentIntent(pendingIntent) .setOngoing(true); if (GBApplication.isRunningLollipopOrLater()) { @@ -64,8 +68,8 @@ public class GB { return builder.build(); } - public static void updateNotification(String text, Context context) { - Notification notification = createNotification(text, context); + public static void updateNotification(String text, boolean connected, Context context) { + Notification notification = createNotification(text, connected, context); updateNotification(notification, NOTIFICATION_ID, context); } @@ -202,18 +206,17 @@ public class GB { * @param ex optional exception to be logged */ public static void toast(final Context context, final String message, final int displayTime, final int severity, final Throwable ex) { + log(message, severity, ex); // log immediately, not delayed if (env().isLocalTest()) { return; } Looper mainLooper = Looper.getMainLooper(); if (Thread.currentThread() == mainLooper.getThread()) { - log(message, severity, ex); Toast.makeText(context, message, displayTime).show(); } else { Runnable runnable = new Runnable() { @Override public void run() { - log(message, severity, ex); Toast.makeText(context, message, displayTime).show(); } }; @@ -226,7 +229,7 @@ public class GB { } } - private static void log(String message, int severity, Throwable ex) { + public static void log(String message, int severity, Throwable ex) { switch (severity) { case INFO: LOG.info(message, ex); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java new file mode 100644 index 000000000..e895919d3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java @@ -0,0 +1,16 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +public class GBPrefs { + + public static final String AUTO_RECONNECT = "general_autocreconnect"; + public static boolean AUTO_RECONNECT_DEFAULT = true; + private final Prefs mPrefs; + + public GBPrefs(Prefs prefs) { + mPrefs = prefs; + } + + public boolean getAutoReconnect() { + return mPrefs.getBoolean(AUTO_RECONNECT, AUTO_RECONNECT_DEFAULT); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LimitedQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LimitedQueue.java index c69cc0de7..eb8ef7aa2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LimitedQueue.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LimitedQueue.java @@ -23,7 +23,7 @@ public class LimitedQueue { public void remove(int id) { for (Iterator iter = list.iterator(); iter.hasNext(); ) { Pair pair = iter.next(); - if (((int)pair.first) == id) { + if ((Integer) pair.first == id) { iter.remove(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java new file mode 100644 index 000000000..11ec96782 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java @@ -0,0 +1,28 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +public class PebbleUtils { + public static String getPlatformName(String hwRev) { + String platformName; + if (hwRev.startsWith("snowy")) { + platformName = "basalt"; + } else if (hwRev.startsWith("spalding")) { + platformName = "chalk"; + } else { + platformName = "aplite"; + } + return platformName; + } + + public static String getModel(String hwRev) { + //TODO: get real data? + String model; + if (hwRev.startsWith("snowy")) { + model = "pebble_time_black"; + } else if (hwRev.startsWith("spalding")) { + model = "pebble_time_round_black_20mm"; + } else { + model = "pebble_black"; + } + return model; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java new file mode 100644 index 000000000..03aca1aa2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java @@ -0,0 +1,152 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +import android.content.SharedPreferences; +import android.util.Log; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +/** + * Wraps SharedPreferences to avoid ClassCastExceptions and others. + */ +public class Prefs { + private static final String TAG = "Prefs"; + // DO NOT use slf4j logger here, this would break its configuration via GBApplication +// private static final Logger LOG = LoggerFactory.getLogger(Prefs.class); + + private final SharedPreferences preferences; + + public Prefs(SharedPreferences preferences) { + this.preferences = preferences; + } + + public String getString(String key, String defaultValue) { + String value = preferences.getString(key, defaultValue); + if (value == null || "".equals(value)) { + return defaultValue; + } + return value; + } + + public Set getStringSet(String key, Set defaultValue) { + Set value = preferences.getStringSet(key, defaultValue); + if (value == null || value.isEmpty()) { + return defaultValue; + } + return value; + } + + /** + * Returns the preference saved under the given key as an integer value. + * Note that it is irrelevant whether the preference value was actually + * saved as an integer value or a string value. + * @param key the preference key + * @param defaultValue the default value to return if the preference value is unset + * @return the saved preference value or the given defaultValue + */ + public int getInt(String key, int defaultValue) { + try { + return preferences.getInt(key, defaultValue); + } catch (Exception ex) { + try { + String value = preferences.getString(key, String.valueOf(defaultValue)); + if ("".equals(value)) { + return defaultValue; + } + return Integer.parseInt(value); + } catch (Exception ex2) { + logReadError(key, ex); + return defaultValue; + } + } + } + + /** + * Returns the preference saved under the given key as a long value. + * Note that it is irrelevant whether the preference value was actually + * saved as a long value or a string value. + * @param key the preference key + * @param defaultValue the default value to return if the preference value is unset + * @return the saved preference value or the given defaultValue + */ + public long getLong(String key, long defaultValue) { + try { + return preferences.getLong(key, defaultValue); + } catch (Exception ex) { + try { + String value = preferences.getString(key, String.valueOf(defaultValue)); + if ("".equals(value)) { + return defaultValue; + } + return Long.parseLong(value); + } catch (Exception ex2) { + logReadError(key, ex); + return defaultValue; + } + } + } + + /** + * Returns the preference saved under the given key as a float value. + * Note that it is irrelevant whether the preference value was actually + * saved as a float value or a string value. + * @param key the preference key + * @param defaultValue the default value to return if the preference value is unset + * @return the saved preference value or the given defaultValue + */ + public float getFloat(String key, float defaultValue) { + try { + return preferences.getFloat(key, defaultValue); + } catch (Exception ex) { + try { + String value = preferences.getString(key, String.valueOf(defaultValue)); + if ("".equals(value)) { + return defaultValue; + } + return Float.parseFloat(value); + } catch (Exception ex2) { + logReadError(key, ex); + return defaultValue; + } + } + } + + /** + * Returns the preference saved under the given key as a boolean value. + * Note that it is irrelevant whether the preference value was actually + * saved as a boolean value or a string value. + * @param key the preference key + * @param defaultValue the default value to return if the preference value is unset + * @return the saved preference value or the given defaultValue + */ + public boolean getBoolean(String key, boolean defaultValue) { + try { + return preferences.getBoolean(key, defaultValue); + } catch (Exception ex) { + try { + String value = preferences.getString(key, String.valueOf(defaultValue)); + if ("".equals(value)) { + return defaultValue; + } + return Boolean.parseBoolean(value); + } catch (Exception ex2) { + logReadError(key, ex); + return defaultValue; + } + } + } + + private void logReadError(String key, Exception ex) { + Log.e(TAG, "Error reading preference value: " + key + "; returning default value", ex); // log the first exception + } + + /** + * Access to the underlying SharedPreferences, typically only used for editing values. + * @return the underlying SharedPreferences object. + */ + public SharedPreferences getPreferences() { + return preferences; + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_device_default_disabled.png b/app/src/main/res/drawable-hdpi/ic_device_default_disabled.png new file mode 100644 index 000000000..694e9cd94 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_default_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_miband_disabled.png b/app/src/main/res/drawable-hdpi/ic_device_miband_disabled.png new file mode 100644 index 000000000..f9f7be9c7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_miband_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_pebble_disabled.png b/app/src/main/res/drawable-hdpi/ic_device_pebble_disabled.png new file mode 100644 index 000000000..8eb1835a8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_pebble_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_information_outline_grey600_24dp.png b/app/src/main/res/drawable-hdpi/ic_information_outline_grey600_24dp.png new file mode 100644 index 000000000..28ac3bdda Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_information_outline_grey600_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_notification_disconnected.png b/app/src/main/res/drawable-hdpi/ic_notification_disconnected.png new file mode 100644 index 000000000..5d510a7a9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_disconnected.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_default_disabled.png b/app/src/main/res/drawable-mdpi/ic_device_default_disabled.png new file mode 100644 index 000000000..113ad06c6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_default_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_miband_disabled.png b/app/src/main/res/drawable-mdpi/ic_device_miband_disabled.png new file mode 100644 index 000000000..9d9f007a9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_miband_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_pebble_disabled.png b/app/src/main/res/drawable-mdpi/ic_device_pebble_disabled.png new file mode 100644 index 000000000..924fcb587 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_pebble_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_information_outline_grey600_24dp.png b/app/src/main/res/drawable-mdpi/ic_information_outline_grey600_24dp.png new file mode 100644 index 000000000..213286b7f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_information_outline_grey600_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png b/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png new file mode 100644 index 000000000..f1587ca34 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_default_disabled.png b/app/src/main/res/drawable-xhdpi/ic_device_default_disabled.png new file mode 100644 index 000000000..208c6abe8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_default_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_miband_disabled.png b/app/src/main/res/drawable-xhdpi/ic_device_miband_disabled.png new file mode 100644 index 000000000..401e1750e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_miband_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_pebble_disabled.png b/app/src/main/res/drawable-xhdpi/ic_device_pebble_disabled.png new file mode 100644 index 000000000..f231da8ec Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_pebble_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_information_outline_grey600_24dp.png b/app/src/main/res/drawable-xhdpi/ic_information_outline_grey600_24dp.png new file mode 100644 index 000000000..34b5a962e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_information_outline_grey600_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_disconnected.png b/app/src/main/res/drawable-xhdpi/ic_notification_disconnected.png new file mode 100644 index 000000000..b382a8f75 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_disconnected.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_default_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_default_disabled.png new file mode 100644 index 000000000..b4349b1ba Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_default_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_miband_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_miband_disabled.png new file mode 100644 index 000000000..94dd2c128 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_miband_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_pebble_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_pebble_disabled.png new file mode 100644 index 000000000..24c8f2f56 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_pebble_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_information_outline_grey600_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_information_outline_grey600_24dp.png new file mode 100644 index 000000000..fb2acbc6a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_information_outline_grey600_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_disconnected.png b/app/src/main/res/drawable-xxhdpi/ic_notification_disconnected.png new file mode 100644 index 000000000..cf509b11c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_disconnected.png differ diff --git a/app/src/main/res/drawable/gadgetbridge_img.png b/app/src/main/res/drawable/gadgetbridge_img.png new file mode 100644 index 000000000..cdb748e00 Binary files /dev/null and b/app/src/main/res/drawable/gadgetbridge_img.png differ diff --git a/app/src/main/res/drawable/ic_add_black.png b/app/src/main/res/drawable/ic_add_black.png new file mode 100644 index 000000000..a633259ea Binary files /dev/null and b/app/src/main/res/drawable/ic_add_black.png differ diff --git a/app/src/main/res/drawable/ic_add_white.png b/app/src/main/res/drawable/ic_add_white.png new file mode 100644 index 000000000..3705a5578 Binary files /dev/null and b/app/src/main/res/drawable/ic_add_white.png differ diff --git a/app/src/main/res/drawable/information_outline.xml b/app/src/main/res/drawable/information_outline.xml new file mode 100644 index 000000000..897c21e2d --- /dev/null +++ b/app/src/main/res/drawable/information_outline.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_sleepchart.xml b/app/src/main/res/layout-land/fragment_sleepchart.xml index 5b4e0b409..0761e1b6f 100644 --- a/app/src/main/res/layout-land/fragment_sleepchart.xml +++ b/app/src/main/res/layout-land/fragment_sleepchart.xml @@ -11,7 +11,7 @@ android:layout_weight="40"> - + android:layout_height="wrap_content" + android:layout_marginBottom="10dp"> - + android:id="@+id/alarm_cb_smart_wakeup"/> + + + + + + + - - - - - - - - - - - + android:layout_gravity="center_horizontal" + android:gravity="center_horizontal|bottom"/> + android:gravity="center_horizontal|top"/> @@ -90,20 +74,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/alarm_cb_tue" - android:layout_gravity="center_horizontal|bottom" - android:gravity="center_horizontal|bottom" - android:layout_weight="1" /> + android:layout_gravity="center_horizontal" + android:gravity="center_horizontal|bottom" /> + android:gravity="center_horizontal|top" /> @@ -117,20 +98,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/alarm_cb_wed" - android:layout_gravity="center_horizontal|bottom" - android:gravity="center_horizontal|bottom" - android:layout_weight="1" /> + android:layout_gravity="center_horizontal" + android:gravity="center_horizontal|bottom"/> + android:gravity="center_horizontal|top" /> @@ -144,20 +122,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/alarm_cb_thu" - android:layout_gravity="center_horizontal|bottom" - android:gravity="center_horizontal|bottom" - android:layout_weight="1" /> + android:layout_gravity="center_horizontal" + android:gravity="center_horizontal|bottom" /> + android:gravity="center_horizontal|top" /> @@ -171,20 +146,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/alarm_cb_fri" - android:layout_gravity="center_horizontal|bottom" - android:gravity="center_horizontal|bottom" - android:layout_weight="1" /> + android:layout_gravity="center_horizontal" + android:gravity="center_horizontal|bottom"/> + android:gravity="center_horizontal|top" /> @@ -198,20 +170,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/alarm_cb_sat" - android:layout_gravity="center_horizontal|bottom" - android:gravity="center_horizontal|bottom" - android:layout_weight="1" /> + android:layout_gravity="center_horizontal" + android:gravity="center_horizontal|bottom"/> + android:gravity="center_horizontal|top" /> @@ -225,20 +194,16 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/alarm_cb_sun" - android:layout_gravity="center_horizontal|bottom" - android:gravity="center_horizontal|bottom" - android:layout_weight="1" /> + android:layout_gravity="center_horizontal"/> + android:gravity="center_horizontal|top" /> diff --git a/app/src/main/res/layout/activity_appinstaller.xml b/app/src/main/res/layout/activity_appinstaller.xml index 8846623b0..eb13abc1b 100644 --- a/app/src/main/res/layout/activity_appinstaller.xml +++ b/app/src/main/res/layout/activity_appinstaller.xml @@ -62,6 +62,14 @@ android:layout_below="@+id/installProgressBar" android:layout_marginTop="10dp" /> + + + - + diff --git a/app/src/main/res/layout/activity_controlcenter.xml b/app/src/main/res/layout/activity_controlcenter.xml index 39a5d2c82..1be7d667c 100644 --- a/app/src/main/res/layout/activity_controlcenter.xml +++ b/app/src/main/res/layout/activity_controlcenter.xml @@ -1,36 +1,62 @@ + + + android:paddingTop="0px"> + android:layout_centerHorizontal="true" /> - + + + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="@color/secondarytext" + android:textStyle="italic" + android:text="@string/tap_connected_device_for_app_mananger" /> + diff --git a/app/src/main/res/layout/activity_debug.xml b/app/src/main/res/layout/activity_debug.xml index 799b7ab5b..e031e7543 100644 --- a/app/src/main/res/layout/activity_debug.xml +++ b/app/src/main/res/layout/activity_debug.xml @@ -73,7 +73,7 @@ android:layout_column="0" android:layout_columnSpan="3" android:layout_gravity="fill_horizontal" - android:layout_row="9" + android:layout_row="10" android:text="create test notification" />