Merge branch 'master' into feature-weather

This commit is contained in:
Andreas Shimokawa 2016-05-16 14:15:39 +02:00
commit dafdb1008d
218 changed files with 7255 additions and 1445 deletions

2
.gitignore vendored
View File

@ -29,3 +29,5 @@ proguard/
*.iml
MPChartLib
fw.dirs

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,6 @@
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
@ -54,6 +53,7 @@
android:label="@string/preferences_miband_settings"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:launchMode="singleTop"
android:name=".activities.AppManagerActivity"
android:label="@string/title_activity_appmanager"
android:parentActivityName=".activities.ControlCenter" />
@ -67,6 +67,7 @@
android:parentActivityName=".activities.ControlCenter">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
@ -87,7 +88,6 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\.pbw" />
<data android:pathPattern="/.*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\.pbw" />
@ -98,7 +98,6 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\.pbz" />
<data android:pathPattern="/.*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\.pbz" />
@ -109,7 +108,6 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\.pbl" />
<data android:pathPattern="/.*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\.pbl" />
@ -123,6 +121,7 @@
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<!-- no mimeType filter, needed for CM-derived ROMs? -->
@ -142,7 +141,6 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\.pbw" />
<data android:pathPattern="/.*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\.pbw" />
@ -153,7 +151,6 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\.pbz" />
<data android:pathPattern="/.*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\.pbz" />
@ -164,7 +161,6 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\.pbl" />
<data android:pathPattern="/.*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\.pbl" />
@ -180,10 +176,11 @@
<!-- to receive the firmwares from the donwload content provider -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
</activity>
<service
@ -237,11 +234,11 @@
android:name=".activities.DebugActivity"
android:label="@string/title_activity_debug"
android:parentActivityName=".activities.ControlCenter"
android:windowSoftInputMode="stateHidden"></activity>
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.DiscoveryActivity"
android:label="@string/title_activity_discovery"
android:parentActivityName=".activities.ControlCenter"></activity>
android:parentActivityName=".activities.ControlCenter" />
<activity
android:name=".activities.AndroidPairingActivity"
android:label="@string/title_activity_android_pairing" />
@ -260,7 +257,38 @@
android:name=".activities.AlarmDetails"
android:label="@string/title_activity_alarm_details"
android:parentActivityName=".activities.ConfigureAlarms" />
<provider android:authorities="com.getpebble.android.provider" android:exported="true" android:name=".contentprovider.PebbleContentProvider" />
<provider
android:name=".contentprovider.PebbleContentProvider"
android:authorities="com.getpebble.android.provider"
android:exported="true" />
<receiver android:name=".SleepAlarmWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="nodomain.freeyourgadget.gadgetbridge.SLEEP_ALARM_WIDGET_CLICK" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/sleep_alarm_widget_info" />
</receiver>
<activity
android:name=".activities.ExternalPebbleJSActivity"
android:label="@string/app_configure"
android:parentActivityName=".activities.AppManagerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter" />
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="gadgetbridge" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<meta name='viewport' content='initial-scale=1.0, maximum-scale=1.0'>
<script type="text/javascript" src="js/Uri.js">
</script>
<script type="text/javascript" src="js/gadgetbridge_boilerplate.js">
</script>
<script type="text/javascript">
</script>
<style>
body {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-touch-callout: none;
}
#config_url,#jsondata {
word-wrap: break-word;
margin: 20px;
}
.btn {
display: inline-block;
position: relative;
height: 32px;
line-height: 32px;
border-radius: 2px;
font-size: 0.9em;
background-color: #eee;
color: #646464;
text-align: center;
border-style: none;
}
.btn:active {
border-style: none;
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2)inset;
transition-delay: 0s;
}
<!-- TODO -->
</style>
</head>
<body onload="" style="width: 100%;">
<div id="step1">
<h2>Url of the configuration:</h2>
<div id="config_url"></div>
<!--<button class="btn" name="show config" value="show config" onclick="Pebble.showConfiguration()" >Show config / URL</button>-->
<button class="btn" name="open config" value="open config" onclick="Pebble.actuallyOpenURL()" >Open configuration website</button>
</div>
<div id="step2">
<h2>Incoming configuration data:</h2>
<div id="jsondata"></div>
<button class="btn" name="send config" value="send config" onclick="Pebble.actuallySendData()" >Send data to pebble</button>
</div>
</body>

View File

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

View File

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

View File

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg2"
viewBox="0 0 1814.1732 1814.1732"
height="512mm"
width="512mm"
inkscape:version="0.91 r13725"
sodipodi:docname="211ad552-a817-11e5-98b9-385bef8cd413.svg">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1600"
inkscape:window-height="876"
id="namedview4212"
showgrid="false"
inkscape:zoom="0.4040408"
inkscape:cx="498.78758"
inkscape:cy="883.13571"
inkscape:window-x="1024"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<defs
id="defs4">
<marker
orient="auto"
refY="0.0"
refX="0.0"
id="marker4512"
style="overflow:visible">
<path
id="path4514"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#1976d2;stroke-width:1pt;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
transform="scale(0.8) translate(12.5,0)" />
</marker>
<marker
style="overflow:visible"
id="Arrow1Lstart"
refX="0.0"
refY="0.0"
orient="auto">
<path
transform="scale(0.8) translate(12.5,0)"
style="fill-rule:evenodd;stroke:#1976d2;stroke-width:1pt;stroke-opacity:1;fill:#ffffff;fill-opacity:1"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
id="path4208" />
</marker>
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<circle
style="fill:#1976d2;fill-opacity:1;stroke:none;stroke-width:5.9000001;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4178"
cx="899.03583"
cy="935.34052"
r="652.55853" />
<path
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:0.24025975;stroke:none;stroke-width:5.9000001;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1180.6138,761.25155 -397.79692,586.48635 230.40242,230.4024 a 652.55853,652.55853 0 0 0 515.1523,-469.1328 L 1180.6138,761.25155 Z"
id="path4178-3" />
<path
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#1976d2;stroke-width:13.60000038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 703.29568,640.10033 c 0,0 -62.09786,90.61312 -84.48779,144.65142 -26.49785,122.73311 44.15742,168.66683 151.63581,319.05845 l -2.08379,245.8099 251.87809,0 0,-245.8099 c 0,0 77.6015,-28.5779 110.2687,-109.23097 30.2773,-74.75286 27.0683,-193.76648 27.0683,-193.76648 z"
id="path4176" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#1976d2;stroke-width:13.60000038;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4136"
width="109.0837"
height="219.8989"
x="1289.9961"
y="43.667068"
ry="43.04707"
transform="matrix(0.8660254,0.5,-0.5,0.8660254,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#1976d2;stroke-width:13.60000038;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4136-5"
width="109.0837"
height="252.79712"
x="1180.0483"
y="40.667068"
ry="49.487179"
transform="matrix(0.8660254,0.5,-0.5,0.8660254,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#1976d2;stroke-width:13.60000038;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4136-8"
width="109.0837"
height="297.81583"
x="1070.1006"
y="37.667068"
ry="58.299976"
transform="matrix(0.8660254,0.5,-0.5,0.8660254,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#1976d2;stroke-width:13.60000038;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4136-1"
width="109.0837"
height="219.8989"
x="958.4209"
y="35.667068"
ry="43.04707"
transform="matrix(0.8660254,0.5,-0.5,0.8660254,0,0)" />
<path
inkscape:connector-curvature="0"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#1976d2;stroke-width:12.19999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 707.67725,640.85267 143.17855,38.54881 c 0,0 39.4733,34.66779 27.90054,81.29964 -10.45038,42.10926 -58.66289,44.51639 -58.66289,44.51639 L 705.87743,773.82104"
id="path4186" />
<rect
style="fill:#ff9800;fill-opacity:1;stroke:#ff9800;stroke-width:12.48825073;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0"
id="rect4628"
width="314.25906"
height="96.066086"
x="738.79462"
y="1163.3024"
ry="7.8193355" />
<rect
style="fill:#1976d2;fill-opacity:1;stroke:#1976d2;stroke-width:8.70396328;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4624"
width="171.0025"
height="47.28043"
x="810.42285"
y="1187.136"
ry="15.011105" />
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

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

View File

@ -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<ILoggingEvent> 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()) {
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());
rememberFileLogger();
}
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);
removeFileLogger();
}
} else {
removeFileLogger();
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<ILoggingEvent> 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<ILoggingEvent> fileLogger) {
try {
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
if (root.isAttached(fileLogger)) {
root.detachAppender(fileLogger);
}
} catch (Throwable ex) {
Log.e("GBApplication", "Error removing logger FILE appender", ex);
}
}
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ApplicationInfo, String> 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<ApplicationInfo> 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<ApplicationInfo>() {
@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<ApplicationInfo> adapter = new ArrayAdapter<ApplicationInfo>(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<ApplicationInfo>() {
@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;
}
};

View File

@ -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<GBDeviceApp> appList = new ArrayList<>();
private GBDeviceAppAdapter mGBDeviceAppAdapter;
private GBDeviceApp selectedApp = null;
private GBDevice mGBDevice = null;
private List<GBDeviceApp> getSystemApps() {
List<GBDeviceApp> systemApps = new ArrayList<>();
systemApps.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
if (mGBDevice != null && !"aplite".equals(PebbleUtils.getPlatformName(mGBDevice.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);
}

View File

@ -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<String>());
Prefs prefs = GBApplication.getPrefs();
preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
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<String>());
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<String>());
int reservedSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0);
mGBAlarmListAdapter.setAlarmList(preferencesAlarmListSet, reservedSlots);
mGBAlarmListAdapter.notifyDataSetChanged();

View File

@ -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<? extends Activity> 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<String> wantedPermissions = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.BLUETOOTH);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.BLUETOOTH_ADMIN);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_CONTACTS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.CALL_PHONE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_PHONE_STATE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.PROCESS_OUTGOING_CALLS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.PROCESS_OUTGOING_CALLS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_SMS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.SEND_SMS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_CALENDAR);
if (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);
}
}

View File

@ -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,16 +65,23 @@ 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);
}

View File

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

View File

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

View File

@ -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<ItemWithDetails> mItems = new ArrayList<>();
private ItemWithDetailsAdapter mItemAdapter;
private ListView detailsListView;
private ItemWithDetailsAdapter mDetailsItemAdapter;
private ArrayList<ItemWithDetails> mDetails = new ArrayList<>();
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@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<ItemWithDetails> 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();
}
}

View File

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

View File

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

View File

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

View File

@ -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);
@ -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<ActivitySample> samples) {
protected DefaultChartsData refresh(GBDevice gbDevice, List<ActivitySample> 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<String> xLabels = new ArrayList<>(numEntries);
List<BarEntry> activityEntries = new ArrayList<>(numEntries);
boolean hr = supportsHeartrate();
List<Entry> heartrateEntries = hr ? new ArrayList<Entry>(numEntries) : null;
List<Integer> colors = new ArrayList<>(numEntries); // this is kinda inefficient...
int lastHrSampleIndex = -1;
for (int i = 0; i < numEntries; i++) {
ActivitySample sample = samples.get(i);
@ -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<BarDataSet> 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<IBarDataSet> 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.<String>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<BarEntry> values, List<Integer> 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<Entry> 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;
}
}
}

View File

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

View File

@ -0,0 +1,4 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
public abstract class ChartsData {
}

View File

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

View File

@ -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<Measurement> 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 (!addHistoryDataSet(false)) {
return;
}
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));
}
private boolean addHistoryDataSet(boolean force) {
if (mStepsPerMinuteHistoryChart.getData() == null) {
if (mSteps.getTotalSteps() == 0) {
return; // ignore the first default value to keep the "no-data-description" visible
}
// ignore the first default value to keep the "no-data-description" visible
if (force || mSteps.getTotalSteps() > 0) {
LineData data = new LineData();
mStepsPerMinuteHistoryChart.setData(data);
data.addDataSet(mHistorySet);
data.addDataSet(mHeartRateSet);
mStepsPerMinuteHistoryChart.setData(data);
return true;
}
LineData historyData = (LineData) mStepsPerMinuteHistoryChart.getData();
historyData.addXValue("");
historyData.addEntry(new Entry(stepsPerMinute, mHistorySet.getEntryCount()), 0);
mTotalStepsData.notifyDataSetChanged();
mStepsPerMinuteData.notifyDataSetChanged();
mStepsPerMinuteHistoryChart.notifyDataSetChanged();
renderCharts();
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();
}
@ -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<Entry>(), 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<Entry>(), 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

View File

@ -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<ActivitySample> 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<ActivitySample> samples) {
private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List<ActivitySample> 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;
}
}
}

View File

@ -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) {
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
refreshDaySteps(db, mTodayStepsChart, day, device);
refreshWeekBeforeSteps(db, mWeekStepsChart, day, device);
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<Entry> entries = new ArrayList<>();
List<Integer> 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<Integer> legendColors = new ArrayList<>(1);
List<String> legendLabels = new ArrayList<>(1);
legendColors.add(akActivity.color);
legendLabels.add(getContext().getString(R.string.chart_steps));
chart.getLegend().setCustom(legendColors, legendLabels);
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
// List<Integer> legendColors = new ArrayList<>(1);
// List<String> legendLabels = new ArrayList<>(1);
// legendColors.add(akActivity.color);
// legendLabels.add(getContext().getString(R.string.chart_steps));
// chart.getLegend().setCustom(legendColors, legendLabels);
// chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
private List<ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) {
@ -222,4 +236,32 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
protected List<ActivitySample> 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;
}
}
}

View File

@ -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<GBAlarm> {
if (isOn) {
view.setTextColor(Color.BLUE);
} else {
view.setTextColor(Color.BLACK);
view.setTextColor(GBApplication.getTextColor(mContext));
}
}
}

View File

@ -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<GBDevice> {
@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<GBDevice> {
}
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<ItemWithDetails> 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,28 +107,68 @@ public class GBDeviceAdapter extends ArrayAdapter<GBDevice> {
}
}
} else {
batteryLabel.setText("");
batteryStatusLabel.setText("");
}
switch (device.getType()) {
case PEBBLE:
if (device.isConnected()) {
deviceImageView.setImageResource(R.drawable.ic_device_pebble);
} else {
deviceImageView.setImageResource(R.drawable.ic_device_pebble_disabled);
}
break;
case MIBAND:
if (device.isConnected()) {
deviceImageView.setImageResource(R.drawable.ic_device_miband);
} else {
deviceImageView.setImageResource(R.drawable.ic_device_miband_disabled);
}
break;
default:
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)) {
if (device.getHardwareVersion() != null) {
deviceName = deviceName + " " + device.getHardwareVersion();
if (!isUniqueDeviceName(device, deviceName)) {
deviceName = deviceName + " " + device.getShortAddress();
}
} else {
deviceName = deviceName + " " + device.getShortAddress();
}
}
return deviceName;
}

View File

@ -18,7 +18,12 @@ import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
*/
public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
public static final int SIZE_SMALL = 1;
public static final int SIZE_MEDIUM = 2;
public static final int SIZE_LARGE = 3;
private final Context context;
private boolean horizontalAlignment;
private int size = SIZE_MEDIUM;
public ItemWithDetailsAdapter(Context context, List<ItemWithDetails> items) {
super(context, 0, items);
@ -26,6 +31,10 @@ public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
this.context = context;
}
public void setHorizontalAlignment(boolean horizontalAlignment) {
this.horizontalAlignment = horizontalAlignment;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
ItemWithDetails item = getItem(position);
@ -34,7 +43,18 @@ public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (horizontalAlignment) {
view = inflater.inflate(R.layout.item_with_details_horizontal, parent, false);
} else {
switch (size) {
case SIZE_SMALL:
view = inflater.inflate(R.layout.item_with_details_small, parent, false);
break;
default:
view = inflater.inflate(R.layout.item_with_details, parent, false);
break;
}
}
}
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<ItemWithDetails> {
return view;
}
public void setSize(int size) {
this.size = size;
}
public int getSize() {
return size;
}
}

View File

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

View File

@ -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);
@ -115,9 +119,10 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl
* @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 {
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(cursor.getColumnIndex(KEY_TIMESTAMP)),
cursor.getShort(cursor.getColumnIndex(KEY_INTENSITY)),
cursor.getShort(cursor.getColumnIndex(KEY_STEPS)),
(byte) cursor.getShort(cursor.getColumnIndex(KEY_TYPE)));
cursor.getInt(colTimeStamp),
cursor.getInt(colIntensity),
cursor.getInt(colSteps),
cursor.getInt(colType),
cursor.getInt(colCustomShort));
samples.add(sample);
} while (cursor.moveToNext());
}
}
}
@ -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;
}
}

View File

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

View File

@ -24,9 +24,16 @@ public interface DBHandler {
List<ActivitySample> 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);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_STEPS_PER_DAY;
/**
* Adds a table "STEPS_PER_DAY".
*/
public class ActivityDBUpdate_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);
}
}

View File

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

View File

@ -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.
*
* <p/>
* 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<? extends Activity> 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<? extends Activity> 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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
return firmwareInfo.isGenerallyCompatibleWith(device);
}
if (MiBandConst.MI_1A.equals(deviceHW)) {
return getFirmwareVersionMajor() == 5;
public boolean isSingleFirmware() {
return firmwareInfo.isSingleMiBandFirmware();
}
return false;
/**
* @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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,7 +85,7 @@ 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ public class PebbleGadgetBridgeSampleProvider extends MorpheuzSampleProvider {
}
@Override
public byte getID() {
public int getID() {
return SampleProvider.PROVIDER_PEBBLE_GADGETBRIDGE;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> preferencesAlarmListSet = sharedPrefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
Prefs prefs = GBApplication.getPrefs();
Set<String> preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
//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<String> 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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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),
@ -69,11 +69,9 @@ public class CalendarEvents {
evtCursor.getString(7)
);
calendarEventList.add(calEvent);
} while(evtCursor.moveToNext());
}
return true;
}
return false;
}
public class CalendarEvent {

View File

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

View File

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

View File

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

View File

@ -1,9 +1,18 @@
package nodomain.freeyourgadget.gadgetbridge.model;
public interface ItemWithDetails {
import android.os.Parcelable;
public interface ItemWithDetails extends Parcelable, Comparable<ItemWithDetails> {
String getName();
String getDetails();
int getIcon();
/**
* Equality is based on #getName() only.
*
* @param other
*/
boolean equals(Object other);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> 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,11 +348,22 @@ public class DeviceCommunicationService extends Service {
ArrayList<Alarm> 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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -36,6 +36,7 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLEDeviceSupport>
* 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<T extends AbstractBTLEDeviceSupport>
/**
* 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<T extends AbstractBTLEDeviceSupport>
* 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<T extends AbstractBTLEDeviceSupport>
/**
* 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<T extends AbstractBTLEDeviceSupport>
return operationStatus == OperationStatus.RUNNING;
}
public boolean isOperationFinished() {
return operationStatus == OperationStatus.FINISHED;
}
public T getSupport() {
return mSupport;
}

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