Merge branch 'master' into feature-weather

here
Andreas Shimokawa 2016-12-09 21:52:55 +01:00
commit efb1cd389b
316 changed files with 15914 additions and 3627 deletions

View File

@ -2,7 +2,8 @@ language: android
jdk:
- oraclejdk8
- oraclejdk7
# disabled -- we now set sourceCompatibility and targetCompatibility appropriately
# - oraclejdk7
env:
- GRADLE_OPTS="-XX:MaxPermSize=256m"
@ -28,3 +29,5 @@ android:
# if you need to run emulator(s) during your tests
#- sys-img-armeabi-v7a-android-19
#- sys-img-x86-android-17
script: ./gradlew build connectedCheck --info --stacktrace

View File

@ -1,14 +1,164 @@
###Changelog
####Version 0.15.0
* New device: Liveview
* Liveview: initial support (set the time and receive notifications)
* Pebble: log pebble app logs if option is enabled in pebble development settings
* Pebble: notification icons for more apps
* Pebble: Further improve compatibility for watchface configuration
####Version 0.14.4
* Pebble 2/LE: Fix multiple bugs in reconnection code, honor reconnect tries from settings
* Mi Band 2: Experimental support for activity recognition
* Mi Band 2: Fix time setting code
####Version 0.14.3
* Pebble: Experimental support for pairing and using all Pebble models via BLE
* Mi Band 1: Fix regression causing display of wrong activity data (#440)
* Mi Band 2: Support for continuous heart rate measurements in live activity view
####Version 0.14.2
* Pebble 2: Fix a bug where the Pebble got disconnected by other unrelated LE devices
####Version 0.14.1
* Mi Band 2: Initial experimental support for activity data
* Mi Band 2: Send the fitness goal (steps) to the band
* Pebble 2: Work around firmware installation issues (tested with upgrading 4.2 to 4.3)
* Pebble: Further improve compatibility for watchface configuration
* Pebble: add Kickstart watch face to app manager on FW 4.x
* Charts: display the total time range, not just the range with available data
####Version 0.14.0
* Pebble 2: Initial experimental support for P2/PT2 using BLE
* Pebble: Special support in device discovery activity (MUST be used to get Pebble 2 working)
* Pebble: Improve compatibility for watchface configuration
* Mi Band 2: support for heart rate measurement during sleep
* Mi Band 2: configuration option to activate the display on lift
* Mi Band 2: configuration option to display the time + date or just the time
* Mi Band 2: honor the wear location configuration option
####Version 0.13.9
* Pebble: use the last known location for setting sunrise and sunset
* Pebble: fix Health disappearing forever when deactivating through app manager (and get it back for affected users)
* Mi Band 2: More fixes for connection issues (#408)
####Version 0.13.8
* Mi Band 2: fix connection issues for users of Mi Fit (#408, #425)
* Mi Band 1A: fix firmware update for certain 1A models
####Version 0.13.7
* Pebble: Fix configuration of certain pebble apps (eg. QR Generator, Squared 4.0)
* Pebble: Add context menu option in app manager to search a watchapp in the pebble appstore
* Mi Band: allow to delete Mi Band address from development settings
* Mi Band 2: Initial support for heart rate readings (Debug activity only)
* Mi Band 2: Support disabled alarms
* Attempt to fix spurious device discovery problems
* Correctly recognize Toffeed, Slimsocial and MaterialFBook as facebook notification sources
####Version 0.13.6
* Mi Band 2: Support for multiple alarms (3 at the moment)
* Mi Band 2: Fix for alarms not working when just one is enabled
####Version 0.13.5
* Mi Band 2: Support setting one alarm
* Pebble: Health compatibility for Firmware 4.2
* Improve support for K9 when generic notifications are used (K9 notifications set to never)
####Version 0.13.4
* Mi Band: Initial support for recording heart and displaying rate values
* Mi Band: Support for testing vibration patterns directly from the preferences
* Mi Band: Clean up vibration preferences
* Possibly fix logging to file on certain devices (#406)
* Mi Band 2: Possibly fix weird connection interdependency between Mi 1 and 2 (#323)
* Mi Band 1S: Whitelist firmware 4.16.4.22
* Mi Band: try application level pairing again, in order to support data sharing with Mi Fit (#250)
* Pebble: new icons and colours for certain apps
* Debug-screen: added button to test "new functionality", currently live sensor data for Mi Band 1
####Version 0.13.3
* Fix regressions with missing bars and labels in charts
* Allow to set notification type in Debug activity
* Move "Disconnect" back to the bottom of the context menu
* Mi Band 2: Display Message and Phone icons
####Version 0.13.2
* Support deleting devices (and their data) in control center
* Sort devices lexicographically in control center
* Do not forward group summary notifications (could fix some duplicate notifications)
* Pebble: Support for health on FW 4.1
* Mi Band: Fix offline charts not displaying heartrate for Mi 1S
####Version 0.13.1
* Improved BLE scanning for Android 5.0+
* Pebble: try to work around duplicate Telegram messages and support Telegram icon
* Pebble: fix some incompatibilities with certain PebbleKit Android apps
####Version 0.13.0
* Initial working Mi Band 2 support (only notifications, no activity and heart rate support)
* Experimental support for Vibratissimo devices
####Version 0.12.2
* Fix for user attribute database table getting spammed and store sleep and steps goals properly
####Version 0.12.1 (release withdrawn)
* Pebble: Fix activity data being associated with the wrong device and/or user in some cases causing them to invisible in charts
* Remove special handling for Conversations notifications since upstream dropped special pebble support
####Version 0.12.0 (release withdrawn)
* NB: User action needed to migrate existing data!
* Store activity data per device and provider to allow multiple devices of the same kind with separate data. Migration is available, except for Pebble Misfit data. Existing data from multiple devices of the same kind (eg. multiple Mi Bands) will get merged while importing.
* In Control Center, display known devices even when Bluetooth is off
* In Control center, new menu point to launch the new "Database management" activity
* Pebble: Support for Pebble Health on Firmware 4.0
* Pebble: Optionally allow raw Pebble Health data to be stored in database completely (for later interpretation, when we are able to decode it)
* Mi Band: fix displaying of deep sleep vs. light sleep (was inverted)
####Version 0.11.2
* Mi Band: support for devices that cannot pair with the band (#349)
####Version 0.11.1
* Various fixes (including crashes) for location settings
* Pebble: Support Pebble Time 2 emulator (needs recompilation of Gadgetbridge)
* Fix a rare crash when, due to Bluetooth problems, when a device has no name
* Fix activity fetching getting stuck when double tapping (#333)
* Mi Band: in the Device Discovery activity, do not display devices that are already paired
* Mi Band: only allow automatic reconnection on disconnect when the device was previously fully connected
* Mi Band: fix a rare crash when reading data fails due to Bluetooth problems
* Mi Band: log full activity sample to help deciphering activity kinds (#341)
* Mi Band 2: improved discovery mechanism to not rely on MAC addresses (#323)
* Charts: only display heart rate samples on devices that support that
* Add more logging to detect problems with external directories (#343)
####Version 0.11.0
* Pebble: new App Manager (keeps track of installed apps and allows app sorting on FW 3.x)
* Pebble: call dismissal with canned SMS (FW 3.x)
* Pebble: watchapp configuration presets
* Pebble: fix regression with FW 2.x (almost everything was broken in 0.10.2)
####Version 0.10.2
* Pebble: allow to manually paste configuration data for legacy configuration pages
* Pebble: various improvements to the configuration page
* Pebble: Support FW 4.0-dp1 and Pebble2 emulator (needs recompilation of Gadgetbridge)
* Pebble: Fix a problem with key events when using the Pebble music player
####Version 0.10.1
* Pebble: set extended music info by dissecting notifications on Android 5.0+
* Pebble: various other improvements to music playback
* Pebble: allow ignoring activity trackers individually (to keep the data on the pebble)
* Mi Band: support for shifting the device time by N hours (for people who sleep at daytime)
* Mi Band: initial and untested support for Mi Band 2
* Allow setting the application language
####Version 0.10.0
* Pebble: option to send sunrise and sunset events to timeline
* Pebble: fix problems with unknown app keys while configuring watchfaces
* Mi Band: BLE connection fixes
* Fixes for enabling logging at whithout restarting Gadgetbridge
* Fixes for enabling logging at without restarting Gadgetbridge
* Re-enable device paring activity on Android 6 (BLE scanning needs the location preference)
* Display device address in device info
####Version 0.9.8
* Pebble: fix more reconnnect issues
* Pebble: fix more reconnect issues
* Pebble: fix deep sleep not being detected with Firmware 3.12 when using Pebble Health
* Pebble: option in AppManager to delete files from cache
* Pebble: enable pbw cache and watchface configuration for Firmware 2.x
@ -20,7 +170,7 @@
* Pebble: hopefully fix some reconnect issues
* Mi Band: fix live activity monitoring running forever if back button pressed
* Mi Band: allow low latency firmware updates, fixes update with some phones
* Mi Band: inital experimental and probably broken support for Amazfit
* Mi Band: initial experimental and probably broken support for Amazfit
* Show aliases for BT Devices if they had been renamed in BT Settings
* Do not show a hint about App Manager when a Mi Band is connected
@ -102,7 +252,7 @@
####Version 0.7.4
* Refactored the settings activity: User details are now generic instead of miband specific. Old settings are preserved.
* Pebble: Fix regression with broken active reconnect since 0.7.0
* Pebble: Support activation and deactivation of Pebble Health. Activation uses the User details as seen above. Insigths are NOT activated.
* Pebble: Support activation and deactivation of Pebble Health. Activation uses the User details as seen above. Insights are NOT activated.
Please be aware that deactivation does NOT delete the data stored on the watch (but it seems to stop the tracking), and we do not know how to switch to metric length units.
####Version 0.7.3
@ -111,7 +261,7 @@
####Version 0.7.2
* Pebble: Allow replying to generic notifications that contain a wearable reply action (tested with Signal)
* Pebble: Support seting up a common suffix for canned replies (defaults to " (canned reply)")
* Pebble: Support setting up a common suffix for canned replies (defaults to " (canned reply)")
* Mi Band: Avoid NPEs when aborting an erroneous sync #205
* Mi Band: Fix discovery of Mi Band 1S
* Add a confirmation dialog when performing a db import
@ -128,7 +278,7 @@
* Pebble: Allow installing apps compiled with SDK 2.x also on the basalt platform (Time, Time Steel)
* Pebble: Fix decoding strings in appmessages from the pebble (fixes sending SMS from "Dialer for Pebble")
* Pebble: Support incoming reconnections when device returns from "Airplane Mode" or "Stand-By Mode"
* Pebble: Fix crash when turning off bluetooth when connected on Android 6.0
* Pebble: Fix crash when turning off Bluetooth when connected on Android 6.0
* Mi Band: reserve some alarm slots for alerting when upcoming events begin. NB: the band will vibrate at the start time of the event, android reminders are ignored
* Mi Band: Display unique devices Names, not just "MI"
* Some new and updated icons
@ -152,7 +302,7 @@
* Pebble: fix installation of pbw files on firmware 3.x when using content providers (eg. download manager)
* Pebble: fix crash on firmware 3.x when pebble requests a pbw that is not in Gadgetbridge's cache
+ Treat Signal notifications as chat notifications
* Fix crash when contacts cannot be read on Android 6.0 (non-granted pemissions)
* Fix crash when contacts cannot be read on Android 6.0 (non-granted permissions)
####Version 0.6.7
* Pebble: Allow installation of 3.x apps on OG Pebble (FW will be released soon)
@ -186,7 +336,7 @@
* Try to prevent service being killed by disallowing backups
####Version 0.6.2
* Mi Band: support firmare versione 1.0.10.14 (and onwards?) vibration
* Mi Band: support firmware version 1.0.10.14 (and onwards?) vibration
* Mi Band: get device name from official BT SIG endpoint
* Mi Band: initial support for displaying live activity data, screen stays on
@ -198,11 +348,11 @@
* Bugfix for app blacklist (some checkboxes where wrongly drawn as checked)
####Version 0.6.0
* Pebble: WIP implementantion of PebbleKit Intents to make some 3rd party Android apps work with the Pebble (eg. Ventoo)
* Pebble: WIP implementation of PebbleKit Intents to make some 3rd party Android apps work with the Pebble (eg. Ventoo)
* Pebble: Option to set reconnection attempts in settings (one attempt usually takes about 5 seconds)
* Support contolling all audio players that react to media buttons (can be chosen in settings)
* Support controlling all audio players that react to media buttons (can be chosen in settings)
* Treat SMS as generic notification if set to "never" (can be blacklisted there also if desired)
* Treat Conversations messagess as chat messages, even if arrived via Pebble Intents (nice icon for Pebble FW 3.x)
* Treat Conversations messages as chat messages, even if arrived via Pebble Intents (nice icon for Pebble FW 3.x)
* Allow opening firmware / app files from the download manager "app" (technically a content provider)
* Mi Band: whitelisted a few firmware versions
@ -226,7 +376,7 @@
* Graphs are now using the same theme as the rest of the application
* Graphs now show when the device was not worn by the user (for devices that send this information)
* Remove unused settings option in charts view
* Build target is now Android SDK 23 (Marshmellow)
* Build target is now Android SDK 23 (Marshmallow)
####Version 0.5.1
* Pebble: support taking screenshot from Pebble Time
@ -239,7 +389,7 @@
* Pebble: use SMS/EMAIL icons for FW 3.x/Pebble Time
* Pebble: do not throttle notifications
* Support going forward/backwards in time in the activity charts
* Various small bugfixes to the App/Fw Installation Activity
* Various small bugfixes to the App/FW Installation Activity
####Version 0.4.6
* Mi Band: Fixed negative number of steps displayed (#91)
@ -254,13 +404,13 @@
####Version 0.4.5
* Enhancement to activity graphs: new graph showing the number of steps done today and in the last week
* New preference to set the desired fitness goal (number of steps to walk in one day)
* Mi Band: support for setting the fitness goal (the band will show the progress to the goal with the leds and vibrates when the goal is reached)
* Mi Band: support for setting the fitness goal (the band will show the progress to the goal with the LEDs and vibrates when the goal is reached)
* Mi Band: send the wear location (left / right hand) to the device
* Mi Band: support for flashing firmware from .fw files (upgrades and downgrades are possible)
* Fixed crash when synchronizing activity data in the graphs activity and changing device orientation
####Version 0.4.4
* Set GadgetBridge notification visibility to public, to show the connection status on the lockscreen
* Set Gadgetbridge notification visibility to public, to show the connection status on the lockscreen
* Support for backup up and restoring of the activity database (via Debug activity)
* Support for graceful upgrades and downgrades, keeping your activity database intact
* Enhancement to activity graphs: new graphs for sleep data (only last night) accessible swiping right from the main graph
@ -337,7 +487,7 @@
####Version 0.2.0
* Experimental pbw installation support (watchfaces/apps)
* New icons for device and app lists
* Fix for device list not refreshing when bluetooth gets turned on
* Fix for device list not refreshing when Bluetooth gets turned on
* Filter out annoying low battery notifications
* Fix for crash on some devices when creating a debug notification
* Lots of internal changes preparing multi device support
@ -360,8 +510,8 @@
* Remove quit button from the service notification, put a quit item in the context menu instead
####Version 0.1.2
* Added option to start Gadgetbridge and connect automatically when bluetooth is turned on
* stop service if bluetooth is turned off
* Added option to start Gadgetbridge and connect automatically when Bluetooth is turned on
* stop service if Bluetooth is turned off
* try to reconnect if connection was lost
####Version 0.1.1

2
GBDaoGenerator/.gitignore vendored Normal file
View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -1,7 +1,7 @@
Gadgetbridge
============
Gadgetbridge is an Android (4.4+) Application which will allow you to use your
Gadgetbridge is an Android (4.4+) application which will allow you to use your
Pebble or Mi Band without the vendor's closed source application and without the
need to create an account and transmit any of your data to the vendor's servers.
@ -15,7 +15,11 @@ need to create an account and transmit any of your data to the vendor's servers.
## Supported Devices
* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round
* Mi Band, Mi Band 1A, Mi Band 1S (experimental)
* Pebble 2, Pebble Time 2 (experimental, PAIR WITHIN GADGETBRIDGE)
* Mi Band, Mi Band 1A, Mi Band 1S
* Mi Band 2
* Vibratissimo (experimental)
* Liveview
## Features (Pebble)
@ -26,63 +30,80 @@ need to create an account and transmit any of your data to the vendor's servers.
* K-9 Mail notification support
* Support for generic notifications (above filtered out)
* Support for up to 16 predefined replies for SMS and Android Wear compatible notifications (experimental, tested with Signal)
* Dismiss individial notifications, mute or open corresponding app on phone from the action menu (generic notifications)
* Dismiss individual notifications, mute or open corresponding app on phone from the action menu (generic notifications)
* Dismiss all notifications from the action menu (non-generic notifications)
* Music playback info (artist, album, track)
* Music control: play/pause, next track, previous track, volume up, volume down
* List and remove installed apps/watchfaces
* Install watchfaces and watchapps (.pbw)
* Install firwmare files (.pbz) [READ THE WIKI](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble-Firmware-updates)
* Install firmware 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 (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)
## Notes about Firmware >=3.0 (Pebble Time, updated OG)
* Listing installed watchfaces will simply display previously installed watchapps, no matter if they are still installed or not.
* Gadgetbridge will keep track of installed watchfaces, but if the Pebble is used with another phone or another app, the information displayed in the app manager can get out of sync since it is impossible to query Firmware >= 3.x for installed apps/watchfaces.
## Getting Started (Pebble)
1. Pair your Pebble through the Android's Bluetooth Settings
1. Pair your Pebble through the Android's Bluetooth Settings or Gadgetbridge. Pebble 2 MUST be paired though Gadgetbridge (tap on the + in Control Center)
2. Start Gadgetbridge, tap on the device you want to connect to
3. To test, choose "Debug" from the menu and play around
For more information read [this wiki article](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble-Getting-Started)
## Features (Mi Band)
## Features (Mi Band 1x)
* Mi Band notifications (LEDs + vibration) for
* Discovery and pairing
* Discovery and pairing
* Mi Band notifications (LEDs + vibration) for
* Display live activity data (alpha)
* Incoming calls
* SMS received
* K-9 mails received
* Conversations messages
* Generic Android notifications
* Synchronize the time to the Mi Band
* Display firmware version and battery state
* Firmware Update
* Heartrate Measurement (alpha)
* Firmware update
* Heart rate measurement on demand and during sleep
* Synchronize activity data
* Display sleep data (alpha)
* Display sports data (step count) (alpha)
* Display live activity data (alpha)
* Set alarms on the Mi Band
## How to use (Mi Band)
## Features (Mi Band 2)
* When starting Gadgetbridge and no device is visible, it will automatically
attempt to discover and pair your Mi Band. Alternatively you can invoke this
manually via the menu button. It will ask you for some personal info that appears
* Discovery and pairing
* Mi Band notifications (Display + vibration) for
* Incoming calls
* SMS received
* K-9 mails received
* Conversations messages
* Generic Android notifications
* Synchronize the time to the Mi Band 2
* Display firmware version
* Heart rate measurement on demand and during sleep
* Synchronize activity data (alpha)
* Set alarms on the Mi Band 2
## How to use (Mi Band 1+2)
* When starting Gadgetbridge the first time, it will automatically
attempt to discover and pair your Mi Band. Alternatively you can invoke discovery
manually via the "+" button. It will ask you for some personal info that appears
to be needed for proper steps calculation on the band. If you do not provide these,
some hardcoded default "dummy" values will be used instead.
When your Mi Band starts to vibrate and blink with all three LEDs during the pairing process,
When your Mi Band starts to vibrate and blink during the pairing process,
tap it quickly a few times in a row to confirm the pairing with the band.
1. Configure other notifications as desired
2. Go back to the "Gadgetbridge" Activity
3. Tap the "MI" item to connect if you're not connected yet.
2. Go back to the "Gadgetbridge" activity
3. Tap the Mi Band item to connect if you're not connected yet
4. To test, chose "Debug" from the menu and play around
Known Issues:
@ -90,6 +111,14 @@ Known Issues:
* The initial connection to a Mi Band sometimes takes a little patience. Try to connect a few times, wait,
and try connecting again. This only happens until you have "bonded" with the Mi Band, i.e. until it
knows your MAC address. This behavior may also only occur with older firmware versions.
* If you use other apps like Mi Fit, and "bonding" with Gadgetbridge does not work, please
try to unpair the band in the other app and try again with Gadgetbridge.
## Features (Liveview)
* set time (automatically upon connection)
* display notifications and vibrate
## Authors (in order of first code contribution)
@ -109,17 +138,15 @@ Translations can be contributed via https://www.transifex.com/projects/p/gadgetb
Feel free to open an issue on our issue tracker, but please:
- do not use the issue tracker as a forum, do not ask for ETAs and read the issue conversation before posting
- use the search functionality to ensure that your questions wasn't already answered. Don't forget to check the **closed** issues as well!
- use the search functionality to ensure that your question wasn't already answered. Don't forget to check the **closed** issues as well!
- remember that this is a community project, people are contributing in their free time because they like doing so: don't take the fun away! Be kind and constructive.
## Having problems?
1. Open Gadgetbridge's settings and check the option to write log files
2. Quit Gadgetbridge and restart it
3. Reproduce the problem you encountered
4. Check the logfile at /sdcard/Android/data/nodomain.freeyourgadget.gadgetbridge/files/gadgetbridge.log
5. File an issue at https://github.com/Freeyourgadget/Gadgetbridge/issues/new and possibly provide the logfile
2. Reproduce the problem you encountered
3. Check the logfile at /sdcard/Android/data/nodomain.freeyourgadget.gadgetbridge/files/gadgetbridge.log
4. File an issue at https://github.com/Freeyourgadget/Gadgetbridge/issues/new and possibly provide the logfile
Alternatively you may use the standard logcat functionality to access the log.

View File

@ -1,14 +1,22 @@
apply plugin: 'com.android.application'
apply plugin: 'findbugs'
apply plugin: 'pmd'
def ABORT_ON_CHECK_FAILURE=false
tasks.withType(Test) { systemProperty 'MiFirmwareDir', System.getProperty('MiFirmwareDir', null) }
// sourceSets.test.runtimeClasspath += File('src/main/assets')
tasks.withType(Test) {
systemProperty 'MiFirmwareDir', System.getProperty('MiFirmwareDir', null)
systemProperty 'logback.configurationFile', System.getProperty('user.dir', null) + '/app/src/main/assets/logback.xml'
systemProperty 'GB_LOGFILES_DIR', java.nio.file.Files.createTempDirectory('gblog').toString();
}
android {
compileOptions {
// for KitKat
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
compileSdkVersion 23
buildToolsVersion "23.0.3"
@ -18,8 +26,8 @@ android {
targetSdkVersion 23
// note: always bump BOTH versionCode and versionName!
versionName "0.10.0"
versionCode 53
versionName "0.15.0"
versionCode 77
}
buildTypes {
release {
@ -43,11 +51,16 @@ android {
}
}
pmd {
toolVersion = '5.5.1'
}
dependencies {
// testCompile 'ch.qos.logback:logback-classic:1.1.3'
// testCompile 'ch.qos.logback:logback-core:1.1.3'
testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:1.9.5"
testCompile "org.robolectric:robolectric:3.1.2"
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.3.0'
@ -55,10 +68,20 @@ dependencies {
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.2.4'
compile 'com.github.PhilJay:MPAndroidChart:v3.0.1'
compile 'com.github.pfichtner:durationformatter:0.1.1'
compile 'de.cketti.library.changelog:ckchangelog:1.2.2'
compile 'net.e175.klaus:solarpositioning:0.0.9'
compile 'com.github.freeyourgadget:greendao:1998d7cd2d21f662c6044f6ccf3b3a251bbad341'
compile 'com.github.woxthebox:draglistview:1.2.6'
compile 'org.apache.commons:commons-lang3:3.4'
// compile project(":DaoCore")
}
preBuild.dependsOn(":GBDaoGenerator:genSources")
gradle.beforeProject {
preBuild.dependsOn(":GBDaoGenerator:genSources")
}
check.dependsOn 'findbugs', 'pmd', 'lint'

View File

@ -55,7 +55,7 @@
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:launchMode="singleTop"
android:name=".activities.AppManagerActivity"
android:name=".activities.appmanager.AppManagerActivity"
android:label="@string/title_activity_appmanager"
android:parentActivityName=".activities.ControlCenter" />
<activity
@ -174,7 +174,7 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
</intent-filter>
<!-- to receive the firmwares from the donwload content provider -->
<!-- to receive the firmwares from the download content provider -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -182,6 +182,15 @@
<data android:mimeType="application/octet-stream" />
</intent-filter>
<!-- to receive firmwares from the download content provider if recognized as zip-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/x-zip-compressed" />
</intent-filter>
</activity>
<service
@ -231,11 +240,27 @@
</intent-filter>
</receiver>
<!--
forcing the DebugActivity to portrait mode avoids crashes with the progress
dialog when changing orientation
-->
<activity
android:name=".activities.DebugActivity"
android:label="@string/title_activity_debug"
android:parentActivityName=".activities.ControlCenter"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.DbManagementActivity"
android:label="@string/title_activity_db_management"
android:parentActivityName=".activities.ControlCenter"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.OnboardingActivity"
android:label="@string/title_activity_onboarding"
android:parentActivityName=".activities.ControlCenter"
android:screenOrientation="portrait" />
<activity
android:name=".activities.DiscoveryActivity"
android:label="@string/title_activity_discovery"
@ -246,6 +271,9 @@
<activity
android:name=".devices.miband.MiBandPairingActivity"
android:label="@string/title_activity_mi_band_pairing" />
<activity
android:name=".devices.pebble.PebblePairingActivity"
android:label="@string/title_activity_pebble_pairing" />
<activity
android:name=".activities.charts.ChartsActivity"
android:label="@string/title_activity_charts"
@ -258,6 +286,10 @@
android:name=".activities.AlarmDetails"
android:label="@string/title_activity_alarm_details"
android:parentActivityName=".activities.ConfigureAlarms" />
<activity
android:name=".activities.VibrationActivity"
android:label="@string/title_activity_vibration"
android:parentActivityName=".activities.ControlCenter" />
<provider
android:name=".contentprovider.PebbleContentProvider"
@ -276,9 +308,12 @@
</receiver>
<activity
android:launchMode="singleTask"
android:allowTaskReparenting="true"
android:clearTaskOnLaunch="true"
android:name=".activities.ExternalPebbleJSActivity"
android:label="@string/app_configure"
android:parentActivityName=".activities.AppManagerActivity">
android:parentActivityName=".activities.appmanager.AppManagerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter" />

View File

@ -1,6 +1,6 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<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>
@ -19,7 +19,8 @@
}
#config_url,#jsondata {
word-wrap: break-word;
margin: 20px;
margin: 20px 0;
width: 90%;
}
.btn {
display: inline-block;
@ -38,19 +39,52 @@
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2)inset;
transition-delay: 0s;
}
p {
width: 90%;
}
#pastereturn {
width: 90%;
min-height: 3em;
}
#step1compat, #step2 {
display: none;
}
<!-- TODO -->
</style>
</head>
<body onload="" style="width: 100%;">
<div id="step1">
<h2>Url of the configuration:</h2>
<body>
<div id="step1" class="step">
<h2>URL of the configuration:</h2>
<div id="config_url"></div>
<!--<button class="btn" name="show config" value="show config" onclick="Pebble.showConfiguration()" >Show config / URL</button>-->
<button class="btn" name="open config" value="open config" onclick="Pebble.actuallyOpenURL()" >Open configuration website</button>
<button class="btn" name="open config" value="open config" onclick="Pebble.actuallyOpenURL()">
Open configuration website
</button>
<h2 class="load_presets">App presets:</h2>
<button class="btn load_presets" name="read config" value="read config"
onclick="Pebble.loadPreset()">
Load saved configuration
</button>
</div>
<div id="step2">
<div id="step1compat" class="step">
<p>In case of "network error" after saving settings in the watchapp, copy the "network error"
URL and paste it here:</p>
<textarea id="pastereturn"></textarea><br/>
<button class="btn" name="parse" onclick="Pebble.parseReturnedPebbleJS()">Parse legacy app
configuration
</button>
</div>
<div id="step2" class="step">
<h2>Incoming configuration data:</h2>
<div id="jsondata"></div>
<button class="btn" name="send config" value="send config" onclick="Pebble.actuallySendData()" >Send data to pebble</button>
<button class="btn" name="send config" value="send config" onclick="Pebble.actuallySendData()">
Send data to pebble
</button>
<h2 class="store_presets">App Presets:</h2>
<button class="btn store_presets" name="store config" value="store config"
onclick="Pebble.savePreset()">
Store incoming configuration
</button>
<p class="store_presets">Existing presets will be deleted.</p>
</div>
</body>

View File

@ -1,3 +1,21 @@
if (window.Storage){
var prefix = GBjs.getAppLocalstoragePrefix();
GBjs.gbLog("redefining local storage with prefix: " + prefix);
Storage.prototype.setItem = (function(key, value) {
this.call(localStorage,prefix + key, value);
}).bind(Storage.prototype.setItem);
Storage.prototype.getItem = (function(key) {
// console.log("I am about to return " + prefix + key);
var def = null;
if(key == 'clay-settings') {
def = '{}';
}
return this.call(localStorage,prefix + key) || def;
}).bind(Storage.prototype.getItem);
}
function loadScript(url, callback) {
// Adding the script tag to the head as suggested before
var head = document.getElementsByTagName('head')[0];
@ -29,45 +47,78 @@ function getURLVariable(variable, defaultValue) {
return defaultValue || false;
}
function showStep(desiredStep) {
var steps = document.getElementsByClassName("step");
var testStep = null;
for (var i = 0; i < steps.length; i ++) {
if (steps[i].id == desiredStep)
testStep = steps[i].id;
}
if (testStep !== null) {
for (var i = 0; i < steps.length; i ++) {
steps[i].style.display = 'none';
}
document.getElementById(desiredStep).style.display="block";
}
}
function gbPebble() {
this.configurationURL = null;
this.configurationValues = null;
var self = this;
self.events = {};
//events processing: see http://stackoverflow.com/questions/10978311/implementing-events-in-my-own-object
self.addEventListener = function(name, handler) {
if (self.events.hasOwnProperty(name))
self.events[name].push(handler);
else
self.events[name] = [handler];
}
this.addEventListener = function(e, f) {
if(e == 'ready') {
this.ready = f;
}
if(e == 'showConfiguration') {
this.showConfiguration = f;
}
if(e == 'webviewclosed') {
this.parseconfig = f;
}
if(e == 'appmessage') {
this.appmessage = f;
self.removeEventListener = function(name, handler) {
if (!self.events.hasOwnProperty(name))
return;
var index = self.events[name].indexOf(handler);
if (index != -1)
self.events[name].splice(index, 1);
}
self.evaluate = function(name, args) {
if (!self.events.hasOwnProperty(name))
return;
if (!args || !args.length)
args = [];
var evs = self.events[name], l = evs.length;
for (var i = 0; i < l; i++) {
evs[i].apply(null, args);
}
}
this.removeEventListener = function(e, f) {
if(e == 'ready') {
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");
showStep("step1compat");
window.open(self.configurationURL.toString(), "config");
}
this.actuallySendData = function() {
GBjs.sendAppMessage(this.configurationValues);
GBjs.sendAppMessage(self.configurationValues);
GBjs.closeActivity();
}
this.savePreset = function() {
GBjs.saveAppStoredPreset(self.configurationValues);
}
this.loadPreset = function() {
showStep("step2");
var presetElements = document.getElementsByClassName("store_presets");
for (var i = 0; i < presetElements.length; i ++) {
presetElements[i].style.display = 'none';
}
self.configurationValues = GBjs.getAppStoredPreset();
document.getElementById("jsondata").innerHTML=self.configurationValues;
}
//needs to be called like this because of original Pebble function name
@ -75,7 +126,7 @@ function gbPebble() {
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=");
self.configurationURL = new Uri(url).addQueryParam("return_to", "gadgetbridge://"+UUID+"?config=true&json=");
} else {
//TODO: add custom return_to
location.href = url;
@ -89,8 +140,8 @@ function gbPebble() {
this.sendAppMessage = function (dict, callbackAck, callbackNack){
try {
this.configurationValues = JSON.stringify(dict);
document.getElementById("jsondata").innerHTML=this.configurationValues;
self.configurationValues = JSON.stringify(dict);
document.getElementById("jsondata").innerHTML=self.configurationValues;
return callbackAck;
}
catch (e) {
@ -107,31 +158,62 @@ function gbPebble() {
return GBjs.getWatchToken();
}
this.getTimelineToken = function() {
return '';
}
this.showSimpleNotificationOnPebble = function(title, body) {
GBjs.gbLog("app wanted to show: " + title + " body: "+ body);
}
this.ready = function() {
this.showConfiguration = function() {
console.error("This watchapp doesn't support configuration");
GBjs.closeActivity();
}
this.parseReturnedPebbleJS = function() {
var str = document.getElementById('pastereturn').value;
var needle = "pebblejs://close#";
if (str.split(needle)[1] !== undefined) {
var t = new Object();
t.response = decodeURIComponent(str.split(needle)[1]);
self.evaluate('webviewclosed',[t]);
showStep("step2");
} else {
console.error("No valid configuration found in the entered string.");
}
}
}
var Pebble = new gbPebble();
var jsConfigFile = GBjs.getAppConfigurationFile();
var storedPreset = GBjs.getAppStoredPreset();
document.addEventListener('DOMContentLoaded', function(){
if (jsConfigFile != null) {
loadScript(jsConfigFile, function() {
Pebble.evaluate('ready');
if (getURLVariable('config') == 'true') {
document.getElementById('step1').style.display="none";
var json_string = unescape(getURLVariable('json'));
showStep("step2");
var json_string = getURLVariable('json');
var t = new Object();
t.response = json_string;
if (json_string != '')
Pebble.parseconfig(t);
if (json_string != '') {
Pebble.evaluate('webviewclosed',[t]);
}
} else {
document.getElementById('step2').style.display="none";
Pebble.ready();
Pebble.showConfiguration();
if (storedPreset === undefined) {
var presetElements = document.getElementsByClassName("load_presets");
for (var i = 0; i < presetElements.length; i ++) {
presetElements[i].style.display = 'none';
}
}
Pebble.evaluate('showConfiguration');
}
});
}
}, false);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -4,13 +4,12 @@ import android.annotation.TargetApi;
import android.app.Application;
import android.app.NotificationManager;
import android.app.NotificationManager.Policy;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION;
@ -23,13 +22,18 @@ import android.util.TypedValue;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import nodomain.freeyourgadget.gadgetbridge.database.ActivityDatabaseHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBConstants;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
@ -39,8 +43,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
//import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver;
/**
* Main Application class that initializes and provides access to certain things like
* logging and DB access.
@ -48,8 +50,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class GBApplication extends Application {
// Since this class must not log to slf4j, we use plain android.util.Log
private static final String TAG = "GBApplication";
public static final String DATABASE_NAME = "Gadgetbridge";
private static GBApplication context;
private static ActivityDatabaseHandler mActivityDatabaseHandler;
private static final Lock dbLock = new ReentrantLock();
private static DeviceService deviceService;
private static SharedPreferences sharedPrefs;
@ -59,6 +62,7 @@ public class GBApplication extends Application {
private static LimitedQueue mIDSenderLookup = new LimitedQueue(16);
private static Prefs prefs;
private static GBPrefs gbPrefs;
private static LockHandler lockHandler;
/**
* Note: is null on Lollipop and Kitkat
*/
@ -73,20 +77,14 @@ public class GBApplication extends Application {
return dir.getAbsolutePath();
}
};
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case ACTION_QUIT:
quit();
break;
}
}
};
private void quit() {
GB.removeAllNotifications(this);
private DeviceManager deviceManager;
public static void quit() {
GB.log("Quitting Gadgetbridge...", GB.INFO, null);
Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
GBApplication.deviceService().quit();
}
public GBApplication() {
@ -102,6 +100,11 @@ public class GBApplication extends Application {
public void onCreate() {
super.onCreate();
if (lockHandler != null) {
// guard against multiple invocations (robolectric)
return;
}
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs = new Prefs(sharedPrefs);
gbPrefs = new GBPrefs(prefs);
@ -116,22 +119,43 @@ public class GBApplication extends Application {
setupExceptionHandler();
deviceService = createDeviceService();
GB.environment = GBEnvironment.createDeviceEnvironment();
mActivityDatabaseHandler = new ActivityDatabaseHandler(context);
loadBlackList();
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(ACTION_QUIT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
setupDatabase(this);
deviceManager = new DeviceManager(this);
deviceService = createDeviceService();
loadBlackList();
if (isRunningMarshmallowOrLater()) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
}
// for testing DB stuff
// SQLiteDatabase db = mActivityDatabaseHandler.getWritableDatabase();
// db.close();
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level >= TRIM_MEMORY_BACKGROUND) {
if (!hasBusyDevice()) {
DBHelper.clearSession();
}
}
}
/**
* Returns true if at least a single device is busy, e.g synchronizing activity data
* or something similar.
* Note: busy is not the same as connected or initialized!
*/
private boolean hasBusyDevice() {
List<GBDevice> devices = getDeviceManager().getDevices();
for (GBDevice device : devices) {
if (device.isBusy()) {
return true;
}
}
return false;
}
public static void setupLogging(boolean enabled) {
@ -147,6 +171,16 @@ public class GBApplication extends Application {
return prefs.getBoolean("log_to_file", false);
}
static void setupDatabase(Context context) {
DBOpenHelper helper = new DBOpenHelper(context, DATABASE_NAME, null);
SQLiteDatabase db = helper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(db);
if (lockHandler == null) {
lockHandler = new LockHandler();
}
lockHandler.init(daoMaster, helper);
}
public static Context getContext() {
return context;
}
@ -166,6 +200,9 @@ public class GBApplication extends Application {
* when that was not successful
* If acquiring was successful, callers must call #releaseDB when they
* are done (from the same thread that acquired the lock!
* <p>
* Callers must not hold a reference to the returned instance because it
* will be invalidated at some point.
*
* @return the DBHandler
* @throws GBException
@ -174,7 +211,7 @@ public class GBApplication extends Application {
public static DBHandler acquireDB() throws GBException {
try {
if (dbLock.tryLock(30, TimeUnit.SECONDS)) {
return mActivityDatabaseHandler;
return lockHandler;
}
} catch (InterruptedException ex) {
Log.i(TAG, "Interrupted while waiting for DB lock");
@ -231,7 +268,7 @@ public class GBApplication extends Application {
@TargetApi(Build.VERSION_CODES.M)
public static boolean isPriorityNumber(int priorityType, String number) {
NotificationManager.Policy notificationPolicy = notificationManager.getNotificationPolicy();
if(priorityType == Policy.PRIORITY_CATEGORY_MESSAGES) {
if (priorityType == Policy.PRIORITY_CATEGORY_MESSAGES) {
if ((notificationPolicy.priorityCategories & Policy.PRIORITY_CATEGORY_MESSAGES) == Policy.PRIORITY_CATEGORY_MESSAGES) {
return isPrioritySender(notificationPolicy.priorityMessageSenders, number);
}
@ -285,17 +322,35 @@ public class GBApplication extends Application {
}
/**
* Deletes the entire Activity database and recreates it with empty tables.
* Deletes both the old Activity database and the new one recreates it with empty tables.
*
* @return true on successful deletion
*/
public static synchronized boolean deleteActivityDatabase() {
if (mActivityDatabaseHandler != null) {
mActivityDatabaseHandler.close();
mActivityDatabaseHandler = null;
public static synchronized boolean deleteActivityDatabase(Context context) {
// TODO: flush, close, reopen db
if (lockHandler != null) {
lockHandler.closeDb();
}
DBHelper dbHelper = new DBHelper(context);
boolean result = true;
if (dbHelper.existsDB(DBConstants.DATABASE_NAME)) {
result = getContext().deleteDatabase(DBConstants.DATABASE_NAME);
}
result &= getContext().deleteDatabase(DATABASE_NAME);
return result;
}
/**
* Deletes the legacy (pre 0.12) Activity database
*
* @return true on successful deletion
*/
public static synchronized boolean deleteOldActivityDatabase(Context context) {
DBHelper dbHelper = new DBHelper(context);
boolean result = true;
if (dbHelper.existsDB(DBConstants.DATABASE_NAME)) {
result = getContext().deleteDatabase(DBConstants.DATABASE_NAME);
}
boolean result = getContext().deleteDatabase(DBConstants.DATABASE_NAME);
mActivityDatabaseHandler = new ActivityDatabaseHandler(getContext());
return result;
}
@ -365,6 +420,7 @@ public class GBApplication extends Application {
theme.resolveAttribute(android.R.attr.textColor, typedValue, true);
return typedValue.data;
}
public static int getBackgroundColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
@ -379,4 +435,8 @@ public class GBApplication extends Application {
public static GBPrefs getGBPrefs() {
return gbPrefs;
}
public DeviceManager getDeviceManager() {
return deviceManager;
}
}

View File

@ -13,9 +13,8 @@ public class GBEnvironment {
return env;
}
public static GBEnvironment createDeviceEnvironment() {
GBEnvironment env = new GBEnvironment();
return env;
static GBEnvironment createDeviceEnvironment() {
return new GBEnvironment();
}
public final boolean isTest() {

View File

@ -0,0 +1,101 @@
package nodomain.freeyourgadget.gadgetbridge;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
/**
* Provides lowlevel access to the database.
*/
public class LockHandler implements DBHandler {
private DaoMaster daoMaster = null;
private DaoSession session = null;
private SQLiteOpenHelper helper = null;
public LockHandler() {
}
public void init(DaoMaster daoMaster, DBOpenHelper helper) {
if (isValid()) {
throw new IllegalStateException("DB must be closed before initializing it again");
}
if (daoMaster == null) {
throw new IllegalArgumentException("daoMaster must not be null");
}
if (helper == null) {
throw new IllegalArgumentException("helper must not be null");
}
this.daoMaster = daoMaster;
this.helper = helper;
session = daoMaster.newSession();
if (session == null) {
throw new RuntimeException("Unable to create database session");
}
}
@Override
public DaoMaster getDaoMaster() {
return daoMaster;
}
private boolean isValid() {
return daoMaster != null;
}
private void ensureValid() {
if (!isValid()) {
throw new IllegalStateException("LockHandler is not in a valid state");
}
}
@Override
public void close() {
ensureValid();
GBApplication.releaseDB();
}
@Override
public synchronized void openDb() {
if (session != null) {
throw new IllegalStateException("session must be null");
}
// this will create completely new db instances and in turn update this handler through #init()
GBApplication.setupDatabase(GBApplication.getContext());
}
@Override
public synchronized void closeDb() {
if (session == null) {
throw new IllegalStateException("session must not be null");
}
session.clear();
session.getDatabase().close();
session = null;
helper = null;
daoMaster = null;
}
@Override
public SQLiteOpenHelper getHelper() {
ensureValid();
return helper;
}
@Override
public DaoSession getDaoSession() {
ensureValid();
return session;
}
@Override
public SQLiteDatabase getDatabase() {
ensureValid();
return daoMaster.getDatabase();
}
}

View File

@ -125,4 +125,24 @@ public abstract class Logging {
}
return false;
}
public static String formatBytes(byte[] bytes) {
if (bytes == null) {
return "(null)";
}
StringBuilder builder = new StringBuilder(bytes.length * 5);
for (byte b : bytes) {
builder.append(String.format("0x%2x", b));
builder.append(" ");
}
return builder.toString().trim();
}
public static void logBytes(Logger logger, byte[] value) {
if (value != null) {
for (byte b : value) {
logger.warn("DATA: " + String.format("0x%2x", b));
}
}
}
}

View File

@ -17,13 +17,12 @@ 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().
* value is retrieved using ActivityUser.().getSleepDuration().
*/
public class SleepAlarmWidget extends AppWidgetProvider {
@ -71,23 +70,24 @@ public class SleepAlarmWidget extends AppWidgetProvider {
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
if (ACTION.equals(intent.getAction())) {
int userSleepDuration = new ActivityUser().getActivityUserSleepDuration();
int userSleepDuration = new ActivityUser().getSleepDuration();
// 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);
GBAlarm alarm = GBAlarm.createSingleShot(0, true, calendar);
alarm.store();
if (GBApplication.isRunningLollipopOrLater()) {
setAlarmViaAlarmManager(context, calendar.getTimeInMillis());
}
int hours = calendar.get(Calendar.HOUR_OF_DAY);
int minutes = calendar.get(Calendar.MINUTE);
GB.toast(context,
String.format(context.getString(R.string.appwidget_alarms_set), hours, minutes),
Toast.LENGTH_SHORT, GB.INFO);

View File

@ -1,23 +1,13 @@
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;
@ -31,10 +21,9 @@ import nodomain.freeyourgadget.gadgetbridge.R;
* to set that listener in #onCreate, *not* in #onPostCreate, otherwise the value will
* not be displayed.
*/
public abstract class AbstractSettingsActivity extends PreferenceActivity {
public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivity {
private static final Logger LOG = LoggerFactory.getLogger(AbstractSettingsActivity.class);
private AppCompatDelegate delegate;
/**
* A preference value change listener that updates the preference's summary
@ -44,7 +33,7 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity {
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
if (preference instanceof EditTextPreference) {
if (((EditTextPreference) preference).getEditText().getKeyListener().getInputType() == InputType.TYPE_CLASS_NUMBER) {
if ((((EditTextPreference) preference).getEditText().getKeyListener().getInputType() & InputType.TYPE_CLASS_NUMBER) != 0) {
if ("".equals(String.valueOf(value))) {
// reject empty numeric input
return false;
@ -104,15 +93,12 @@ public abstract class AbstractSettingsActivity extends PreferenceActivity {
} else {
setTheme(R.style.GadgetbridgeTheme);
}
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
for (String prefKey : getPreferenceKeysWithSummary()) {
final Preference pref = findPreference(prefKey);
@ -124,67 +110,6 @@ 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
@ -236,19 +161,4 @@ 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

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

View File

@ -1,279 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
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 GBActivity {
public static final String ACTION_REFRESH_APPLIST
= "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist";
private static final Logger LOG = LoggerFactory.getLogger(AppManagerActivity.class);
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(GBApplication.ACTION_QUIT)) {
finish();
} else if (action.equals(ACTION_REFRESH_APPLIST)) {
int appCount = intent.getIntExtra("app_count", 0);
for (Integer i = 0; i < appCount; i++) {
String appName = intent.getStringExtra("app_name" + i.toString());
String appCreator = intent.getStringExtra("app_creator" + i.toString());
UUID uuid = UUID.fromString(intent.getStringExtra("app_uuid" + i.toString()));
GBDeviceApp.Type appType = GBDeviceApp.Type.values()[intent.getIntExtra("app_type" + i.toString(), 0)];
boolean found = false;
for (final ListIterator<GBDeviceApp> iter = appList.listIterator(); iter.hasNext(); ) {
final GBDeviceApp app = iter.next();
if (app.getUUID().equals(uuid)) {
app.setOnDevice(true);
iter.set(app);
found = true;
break;
}
}
if (!found) {
GBDeviceApp app = new GBDeviceApp(uuid, appName, appCreator, "", appType);
app.setOnDevice(true);
appList.add(app);
}
}
mGBDeviceAppAdapter.notifyDataSetChanged();
}
}
};
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<>();
if (prefs.getBoolean("pebble_force_untested", false)) {
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;
}
private List<GBDeviceApp> getCachedApps() {
List<GBDeviceApp> cachedAppList = new ArrayList<>();
File cachePath;
try {
cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache");
} catch (IOException e) {
LOG.warn("could not get external dir while reading pbw cache.");
return cachedAppList;
}
File files[] = cachePath.listFiles();
if (files != null) {
for (File file : files) {
if (file.getName().endsWith(".pbw")) {
String baseName = file.getName().substring(0, file.getName().length() - 4);
//metadata
File jsonFile = new File(cachePath, baseName + ".json");
//configuration
File configFile = new File(cachePath, baseName + "_config.js");
try {
String jsonstring = FileUtils.getStringFromFile(jsonFile);
JSONObject json = new JSONObject(jsonstring);
cachedAppList.add(new GBDeviceApp(json, configFile.exists()));
} catch (Exception e) {
LOG.warn("could not read json file for " + baseName, e.getMessage(), e);
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), baseName, "N/A", "", GBDeviceApp.Type.UNKNOWN));
}
}
}
}
return cachedAppList;
}
@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");
}
prefs = GBApplication.getPrefs();
setContentView(R.layout.activity_appmanager);
ListView appListView = (ListView) findViewById(R.id.appListView);
mGBDeviceAppAdapter = new GBDeviceAppAdapter(this, appList);
appListView.setAdapter(this.mGBDeviceAppAdapter);
appListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView parent, View v, int position, long id) {
UUID uuid = appList.get(position).getUUID();
GBApplication.deviceService().onAppStart(uuid, true);
}
});
registerForContextMenu(appListView);
appList.addAll(getCachedApps());
appList.addAll(getSystemApps());
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
filter.addAction(ACTION_REFRESH_APPLIST);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
GBApplication.deviceService().onAppInfoReq();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.appmanager_context, menu);
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
selectedApp = appList.get(acmi.position);
if (!selectedApp.isInCache()) {
menu.removeItem(R.id.appmanager_app_reinstall);
menu.removeItem(R.id.appmanager_app_delete_cache);
}
if (!PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) {
menu.removeItem(R.id.appmanager_health_activate);
menu.removeItem(R.id.appmanager_health_deactivate);
}
if (selectedApp.getType() == GBDeviceApp.Type.APP_SYSTEM) {
menu.removeItem(R.id.appmanager_app_delete);
}
if (!selectedApp.isConfigurable()) {
menu.removeItem(R.id.appmanager_app_configure);
}
menu.setHeaderTitle(selectedApp.getName());
}
private void removeAppFromList(UUID uuid) {
for (final ListIterator<GBDeviceApp> iter = appList.listIterator(); iter.hasNext(); ) {
final GBDeviceApp app = iter.next();
if (app.getUUID().equals(uuid)) {
iter.remove();
mGBDeviceAppAdapter.notifyDataSetChanged();
break;
}
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.appmanager_health_deactivate:
case R.id.appmanager_app_delete_cache:
String baseName;
try {
baseName = FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID();
} catch (IOException e) {
LOG.warn("could not get external dir while trying to access pbw cache.");
return true;
}
String[] suffixToDelete = new String[]{".pbw", ".json", "_config.js"};
for (String suffix : suffixToDelete) {
File fileToDelete = new File(baseName + suffix);
if (!fileToDelete.delete()) {
LOG.warn("could not delete file from pbw cache: " + fileToDelete.toString());
} else {
LOG.info("deleted file: " + fileToDelete.toString());
}
}
removeAppFromList(selectedApp.getUUID());
// fall through
case R.id.appmanager_app_delete:
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
return true;
case R.id.appmanager_app_reinstall:
File cachePath;
try {
cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID() + ".pbw");
} catch (IOException e) {
LOG.warn("could not get external dir while trying to access pbw cache.");
return true;
}
GBApplication.deviceService().onInstallApp(Uri.fromFile(cachePath));
return true;
case R.id.appmanager_health_activate:
GBApplication.deviceService().onInstallApp(Uri.parse("fake://health"));
return true;
case R.id.appmanager_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);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -3,8 +3,8 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
@ -33,7 +33,6 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import de.cketti.library.changelog.ChangeLog;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -41,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -50,18 +50,17 @@ public class ControlCenter extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ControlCenter.class);
public static final String ACTION_REFRESH_DEVICELIST
= "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;
private final List<GBDevice> deviceList = new ArrayList<>();
private DeviceManager deviceManager;
/**
* Temporary field for the context menu
*/
private GBDevice selectedDevice;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
@ -71,48 +70,20 @@ public class ControlCenter extends GBActivity {
case GBApplication.ACTION_QUIT:
finish();
break;
case ACTION_REFRESH_DEVICELIST:
case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
case DeviceManager.ACTION_DEVICES_CHANGED:
refreshPairedDevices();
break;
case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev.getAddress() != null) {
int index = deviceList.indexOf(dev); // search by address
if (index >= 0) {
deviceList.set(index, dev);
} else {
deviceList.add(dev);
}
GBDevice selectedDevice = deviceManager.getSelectedDevice();
if (selectedDevice != null) {
refreshBusyState(selectedDevice);
enableSwipeRefresh(selectedDevice);
}
updateSelectedDevice(dev);
refreshPairedDevices();
refreshBusyState(dev);
enableSwipeRefresh(selectedDevice);
break;
}
}
};
private void updateSelectedDevice(GBDevice dev) {
if (selectedDevice == null) {
selectedDevice = dev;
} else {
if (!selectedDevice.equals(dev)) {
if (selectedDevice.isConnected() && dev.isConnected()) {
LOG.warn("multiple connected devices -- this is currently not really supported");
selectedDevice = dev; // use the last one that changed
}
if (!selectedDevice.isConnected()) {
selectedDevice = dev; // use the last one that changed
}
}
}
}
private void refreshBusyState(GBDevice dev) {
if (dev.isBusy()) {
if (dev != null && dev.isBusy()) {
swipeLayout.setRefreshing(true);
} else {
boolean wasBusy = swipeLayout.isRefreshing();
@ -120,7 +91,6 @@ public class ControlCenter extends GBActivity {
swipeLayout.setRefreshing(false);
}
}
mGBDeviceAdapter.notifyDataSetChanged();
}
@Override
@ -128,6 +98,8 @@ public class ControlCenter extends GBActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_controlcenter);
deviceManager = ((GBApplication)getApplication()).getDeviceManager();
hintTextView = (TextView) findViewById(R.id.hintTextView);
ListView deviceListView = (ListView) findViewById(R.id.deviceListView);
fab = (FloatingActionButton) findViewById(R.id.fab);
@ -140,12 +112,13 @@ public class ControlCenter extends GBActivity {
}
});
final List<GBDevice> deviceList = deviceManager.getDevices();
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);
GBDevice gbDevice = mGBDeviceAdapter.getItem(position);
if (gbDevice.isInitialized()) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
Class<? extends Activity> primaryActivity = coordinator.getPrimaryActivity();
@ -155,7 +128,7 @@ public class ControlCenter extends GBActivity {
startActivity(startIntent);
}
} else {
GBApplication.deviceService().connect(deviceList.get(position));
GBApplication.deviceService().connect(gbDevice);
}
}
});
@ -172,13 +145,9 @@ public class ControlCenter extends GBActivity {
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(ACTION_REFRESH_DEVICELIST);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
filterLocal.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
registerReceiver(mReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
refreshPairedDevices();
/*
* Ask for permission to intercept notifications on first run.
@ -200,7 +169,7 @@ public class ControlCenter extends GBActivity {
GBApplication.deviceService().start();
enableSwipeRefresh(selectedDevice);
enableSwipeRefresh(deviceManager.getSelectedDevice());
if (GB.isBluetoothEnabled() && deviceList.isEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
startActivity(new Intent(this, DiscoveryActivity.class));
} else {
@ -212,7 +181,7 @@ public class ControlCenter extends GBActivity {
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
selectedDevice = deviceList.get(acmi.position);
selectedDevice = mGBDeviceAdapter.getItem(acmi.position);
if (selectedDevice != null && selectedDevice.isBusy()) {
// no context menu when device is busy
return;
@ -229,6 +198,9 @@ public class ControlCenter extends GBActivity {
if (!coordinator.supportsAlarmConfiguration()) {
menu.removeItem(R.id.controlcenter_configure_alarms);
}
if (!coordinator.supportsActivityTracking()) {
menu.removeItem(R.id.controlcenter_start_sleepmonitor);
}
if (selectedDevice.getState() == GBDevice.State.NOT_CONNECTED) {
menu.removeItem(R.id.controlcenter_disconnect);
@ -254,6 +226,7 @@ public class ControlCenter extends GBActivity {
}
private void fetchActivityData() {
GBDevice selectedDevice = deviceManager.getSelectedDevice();
if (selectedDevice == null) {
return;
}
@ -313,6 +286,11 @@ public class ControlCenter extends GBActivity {
GBApplication.deviceService().onScreenshotReq();
}
return true;
case R.id.controlcenter_delete_device:
if (selectedDevice != null) {
confirmDeleteDevice(selectedDevice);
}
return true;
default:
return super.onContextItemSelected(item);
}
@ -341,11 +319,12 @@ public class ControlCenter extends GBActivity {
Intent debugIntent = new Intent(this, DebugActivity.class);
startActivity(debugIntent);
return true;
case R.id.action_db_management:
Intent dbIntent = new Intent(this, DbManagementActivity.class);
startActivity(dbIntent);
return true;
case R.id.action_quit:
GBApplication.deviceService().quit();
Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(this).sendBroadcast(quitIntent);
GBApplication.quit();
return true;
}
@ -356,25 +335,51 @@ public class ControlCenter extends GBActivity {
startActivity(new Intent(this, DiscoveryActivity.class));
}
private void confirmDeleteDevice(final GBDevice gbDevice) {
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(getString(R.string.controlcenter_delete_device_name, gbDevice.getName()))
.setMessage(R.string.controlcenter_delete_device_dialogmessage)
.setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
if (coordinator != null) {
coordinator.deleteDevice(selectedDevice);
}
DeviceHelper.getInstance().removeBond(selectedDevice);
} catch (Exception ex) {
GB.toast(ControlCenter.this, "Error deleting device: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
} finally {
selectedDevice = null;
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
}
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// do nothing
}
})
.show();
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
unregisterReceiver(mReceiver);
super.onDestroy();
}
private void refreshPairedDevices() {
Set<GBDevice> availableDevices = DeviceHelper.getInstance().getAvailableDevices(this);
deviceList.retainAll(availableDevices);
for (GBDevice availableDevice : availableDevices) {
if (!deviceList.contains(availableDevice)) {
deviceList.add(availableDevice);
}
}
boolean connected = false;
List<GBDevice> deviceList = deviceManager.getDevices();
GBDevice connectedDevice = null;
for (GBDevice device : deviceList) {
if (device.isConnected() || device.isConnecting()) {
connected = true;
connectedDevice = device;
break;
}
}
@ -385,8 +390,8 @@ public class ControlCenter extends GBActivity {
background.setVisibility(View.INVISIBLE);
}
if (connected) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(selectedDevice);
if (connectedDevice != null) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(connectedDevice);
hintTextView.setText(coordinator.getTapString());
} else if (!deviceList.isEmpty()) {
hintTextView.setText(R.string.tap_a_device_to_connect);

View File

@ -0,0 +1,278 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.IntentFilter;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.Collections;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapter;
import nodomain.freeyourgadget.gadgetbridge.database.ActivityDatabaseHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DbManagementActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(DbManagementActivity.class);
private Button exportDBButton;
private Button importDBButton;
private Button importOldActivityDataButton;
private Button deleteOldActivityDBButton;
private Button deleteDBButton;
private TextView dbPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_db_management);
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
dbPath = (TextView) findViewById(R.id.activity_db_management_path);
dbPath.setText(getExternalPath());
exportDBButton = (Button) findViewById(R.id.exportDBButton);
exportDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
exportDB();
}
});
importDBButton = (Button) findViewById(R.id.importDBButton);
importDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
importDB();
}
});
boolean hasOldDB = hasOldActivityDatabase();
int oldDBVisibility = hasOldDB ? View.VISIBLE : View.GONE;
View oldDBTitle = findViewById(R.id.mergeOldActivityDataTitle);
oldDBTitle.setVisibility(oldDBVisibility);
View oldDBText = findViewById(R.id.mergeOldActivityDataText);
oldDBText.setVisibility(oldDBVisibility);
importOldActivityDataButton = (Button) findViewById(R.id.mergeOldActivityData);
importOldActivityDataButton.setVisibility(oldDBVisibility);
importOldActivityDataButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mergeOldActivityDbContents();
}
});
deleteOldActivityDBButton = (Button) findViewById(R.id.deleteOldActivityDB);
deleteOldActivityDBButton.setVisibility(oldDBVisibility);
deleteOldActivityDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
deleteOldActivityDbFile();
}
});
deleteDBButton = (Button) findViewById(R.id.emptyDBButton);
deleteDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
deleteActivityDatabase();
}
});
}
private boolean hasOldActivityDatabase() {
return new DBHelper(this).getOldActivityDatabaseHandler() != null;
}
private String getExternalPath() {
try {
return FileUtils.getExternalFilesDir().getAbsolutePath();
} catch (Exception ex) {
LOG.warn("Unable to get external files dir", ex);
}
return getString(R.string.dbmanagementactivvity_cannot_access_export_path);
}
private void exportDB() {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DBHelper helper = new DBHelper(this);
File dir = FileUtils.getExternalFilesDir();
File destFile = helper.exportDB(dbHandler, dir);
GB.toast(this, getString(R.string.dbmanagementactivity_exported_to, destFile.getAbsolutePath()), Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(this, getString(R.string.dbmanagementactivity_error_exporting_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
private void importDB() {
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.dbmanagementactivity_import_data_title)
.setMessage(R.string.dbmanagementactivity_overwrite_database_confirmation)
.setPositiveButton(R.string.dbmanagementactivity_overwrite, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DBHelper helper = new DBHelper(DbManagementActivity.this);
File dir = FileUtils.getExternalFilesDir();
SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();
File sourceFile = new File(dir, sqLiteOpenHelper.getDatabaseName());
helper.importDB(dbHandler, sourceFile);
helper.validateDB(sqLiteOpenHelper);
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_import_successful), Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.show();
}
private void mergeOldActivityDbContents() {
final DBHelper helper = new DBHelper(getBaseContext());
final ActivityDatabaseHandler oldHandler = helper.getOldActivityDatabaseHandler();
if (oldHandler == null) {
GB.toast(this, getString(R.string.dbmanagementactivity_no_old_activitydatabase_found), Toast.LENGTH_LONG, GB.ERROR);
return;
}
selectDeviceForMergingActivityDatabaseInto(new DeviceSelectionCallback() {
@Override
public void invoke(final GBDevice device) {
if (device == null) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_no_connected_device), Toast.LENGTH_LONG, GB.ERROR);
return;
}
try (DBHandler targetHandler = GBApplication.acquireDB()) {
final ProgressDialog progress = ProgressDialog.show(DbManagementActivity.this, getString(R.string.dbmanagementactivity_merging_activity_data_title), getString(R.string.dbmanagementactivity_please_wait_while_merging), true, false);
new AsyncTask<Object, ProgressDialog, Object>() {
@Override
protected Object doInBackground(Object[] params) {
helper.importOldDb(oldHandler, device, targetHandler);
if (!isFinishing() && !isDestroyed()) {
progress.dismiss();
}
return null;
}
}.execute((Object[]) null);
} catch (Exception ex) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_old_activity_data), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
});
}
private void selectDeviceForMergingActivityDatabaseInto(final DeviceSelectionCallback callback) {
GBDevice connectedDevice = ((GBApplication)getApplication()).getDeviceManager().getSelectedDevice();
if (connectedDevice == null) {
callback.invoke(null);
return;
}
final List<GBDevice> availableDevices = Collections.singletonList(connectedDevice);
GBDeviceAdapter adapter = new GBDeviceAdapter(getBaseContext(), availableDevices);
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.dbmanagementactivity_associate_old_data_with_device)
.setAdapter(adapter, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
GBDevice device = availableDevices.get(which);
callback.invoke(device);
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// ignore, just return
}
})
.show();
}
private void deleteActivityDatabase() {
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.dbmanagementactivity_delete_activity_data_title)
.setMessage(R.string.dbmanagementactivity_really_delete_entire_db)
.setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (GBApplication.deleteActivityDatabase(DbManagementActivity.this)) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_database_successfully_deleted), Toast.LENGTH_SHORT, GB.INFO);
} else {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_db_deletion_failed), Toast.LENGTH_SHORT, GB.INFO);
}
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.show();
}
private void deleteOldActivityDbFile() {
new AlertDialog.Builder(this).setCancelable(true);
new AlertDialog.Builder(this).setTitle(R.string.dbmanagementactivity_delete_old_activity_db);
new AlertDialog.Builder(this).setMessage(R.string.dbmanagementactivity_delete_old_activitydb_confirmation);
new AlertDialog.Builder(this).setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (GBApplication.deleteOldActivityDatabase(DbManagementActivity.this)) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_old_activity_db_successfully_deleted), Toast.LENGTH_SHORT, GB.INFO);
} else {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_old_activity_db_deletion_failed), Toast.LENGTH_SHORT, GB.INFO);
}
}
});
new AlertDialog.Builder(this).setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
new AlertDialog.Builder(this).show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
public interface DeviceSelectionCallback {
void invoke(GBDevice device);
}
}

View File

@ -1,14 +1,11 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v4.app.NotificationCompat;
@ -16,33 +13,27 @@ import android.support.v4.app.RemoteInput;
import android.support.v4.content.LocalBroadcastManager;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
import net.e175.klaus.solarpositioning.DeltaT;
import net.e175.klaus.solarpositioning.SPA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.GregorianCalendar;
import java.util.ArrayList;
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.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class DebugActivity extends GBActivity {
@ -52,8 +43,8 @@ public class DebugActivity extends GBActivity {
private static final String ACTION_REPLY
= "nodomain.freeyourgadget.gadgetbridge.DebugActivity.action.reply";
private Button sendSMSButton;
private Button sendEmailButton;
private Spinner sendTypeSpinner;
private Button sendButton;
private Button incomingCallButton;
private Button outgoingCallButton;
private Button startCallButton;
@ -63,9 +54,7 @@ public class DebugActivity extends GBActivity {
private Button setTimeButton;
private Button rebootButton;
private Button HeartRateButton;
private Button exportDBButton;
private Button importDBButton;
private Button deleteDBButton;
private Button testNewFunctionalityButton;
private EditText editContent;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@ -105,27 +94,26 @@ public class DebugActivity extends GBActivity {
registerReceiver(mReceiver, filter); // for ACTION_REPLY
editContent = (EditText) findViewById(R.id.editContent);
sendSMSButton = (Button) findViewById(R.id.sendSMSButton);
sendSMSButton.setOnClickListener(new View.OnClickListener() {
ArrayList<String> spinnerArray = new ArrayList<>();
for (NotificationType notificationType : NotificationType.values()) {
spinnerArray.add(notificationType.name());
}
ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, spinnerArray);
sendTypeSpinner = (Spinner) findViewById(R.id.sendTypeSpinner);
sendTypeSpinner.setAdapter(spinnerArrayAdapter);
sendButton = (Button) findViewById(R.id.sendButton);
sendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.phoneNumber = editContent.getText().toString();
notificationSpec.body = editContent.getText().toString();
notificationSpec.type = NotificationType.SMS;
notificationSpec.id = -1;
GBApplication.deviceService().onNotification(notificationSpec);
}
});
sendEmailButton = (Button) findViewById(R.id.sendEmailButton);
sendEmailButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.sender = getResources().getText(R.string.app_name).toString();
notificationSpec.subject = editContent.getText().toString();
notificationSpec.body = editContent.getText().toString();
notificationSpec.type = NotificationType.EMAIL;
String testString = editContent.getText().toString();
notificationSpec.phoneNumber = testString;
notificationSpec.body = testString;
notificationSpec.sender = testString;
notificationSpec.subject = testString;
notificationSpec.type = NotificationType.values()[sendTypeSpinner.getSelectedItemPosition()];
notificationSpec.id = -1;
GBApplication.deviceService().onNotification(notificationSpec);
}
@ -171,29 +159,6 @@ public class DebugActivity extends GBActivity {
}
});
exportDBButton = (Button) findViewById(R.id.exportDBButton);
exportDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
exportDB();
}
});
importDBButton = (Button) findViewById(R.id.importDBButton);
importDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
importDB();
}
});
deleteDBButton = (Button) findViewById(R.id.emptyDBButton);
deleteDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
deleteActivityDatabase();
}
});
rebootButton = (Button) findViewById(R.id.rebootButton);
rebootButton.setOnClickListener(new View.OnClickListener() {
@Override
@ -250,81 +215,18 @@ public class DebugActivity extends GBActivity {
testNotification();
}
});
}
private void exportDB() {
DBHandler dbHandler = null;
try {
dbHandler = GBApplication.acquireDB();
DBHelper helper = new DBHelper(this);
File dir = FileUtils.getExternalFilesDir();
File destFile = helper.exportDB(dbHandler.getHelper(), dir);
GB.toast(this, "Exported to: " + destFile.getAbsolutePath(), Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(this, "Error exporting DB: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
} finally {
if (dbHandler != null) {
dbHandler.release();
testNewFunctionalityButton = (Button) findViewById(R.id.testNewFunctionality);
testNewFunctionalityButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
testNewFunctionality();
}
}
});
}
private void importDB() {
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle("Import Activity Data?")
.setMessage("Really overwrite the current activity database? All your activity data (if any) will be lost.")
.setPositiveButton("Overwrite", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
DBHandler dbHandler = null;
try {
dbHandler = GBApplication.acquireDB();
DBHelper helper = new DBHelper(DebugActivity.this);
File dir = FileUtils.getExternalFilesDir();
SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();
File sourceFile = new File(dir, sqLiteOpenHelper.getDatabaseName());
helper.importDB(sqLiteOpenHelper, sourceFile);
helper.validateDB(sqLiteOpenHelper);
GB.toast(DebugActivity.this, "Import successful.", Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(DebugActivity.this, "Error importing DB: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
} finally {
if (dbHandler != null) {
dbHandler.release();
}
}
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.show();
}
private void deleteActivityDatabase() {
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle("Delete Activity Data?")
.setMessage("Really delete the entire activity database? All your activity data will be lost.")
.setPositiveButton("Delete", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (GBApplication.deleteActivityDatabase()) {
GB.toast(DebugActivity.this, "Activity database successfully deleted.", Toast.LENGTH_SHORT, GB.INFO);
} else {
GB.toast(DebugActivity.this, "Activity database deletion failed.", Toast.LENGTH_SHORT, GB.INFO);
}
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.show();
private void testNewFunctionality() {
GBApplication.deviceService().onTestNewFunction();
}
private void testNotification() {
@ -379,4 +281,7 @@ public class DebugActivity extends GBActivity {
unregisterReceiver(mReceiver);
}
public interface DeviceSelectionCallback {
void invoke(GBDevice device);
}
}

View File

@ -1,18 +1,26 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.support.v4.app.ActivityCompat;
import android.view.View;
@ -26,19 +34,28 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static android.bluetooth.le.ScanSettings.MATCH_MODE_STICKY;
import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_LATENCY;
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
private ScanCallback newLeScanCallback = null;
private final Handler handler = new Handler();
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@ -46,16 +63,27 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
discoveryStarted(Scanning.SCANNING_BT);
if (isScanning != Scanning.SCANNING_BTLE && isScanning != Scanning.SCANNING_NEW_BTLE) {
discoveryStarted(Scanning.SCANNING_BT);
}
break;
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
// continue with LE scan, if available
if (isScanning == Scanning.SCANNING_BT) {
checkAndRequestLocationPermission();
startDiscovery(Scanning.SCANNING_BTLE);
} else {
discoveryFinished();
}
handler.post(new Runnable() {
@Override
public void run() {
// continue with LE scan, if available
if (isScanning == Scanning.SCANNING_BT) {
checkAndRequestLocationPermission();
if (GBApplication.isRunningLollipopOrLater()) {
startDiscovery(Scanning.SCANNING_NEW_BTLE);
} else {
startDiscovery(Scanning.SCANNING_BTLE);
}
} else {
discoveryFinished();
}
}
});
break;
case BluetoothAdapter.ACTION_STATE_CHANGED:
int oldState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.STATE_OFF);
@ -68,6 +96,14 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
handleDeviceFound(device, rssi);
break;
}
case BluetoothDevice.ACTION_UUID: {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
ParcelUuid[] uuids2 = AndroidUtils.toParcelUUids(uuids);
handleDeviceFound(device, rssi, uuids2);
break;
}
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null && device.getAddress().equals(bondingAddress)) {
@ -85,10 +121,54 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
LOG.warn(device.getName() + ": " + ((scanRecord != null) ? scanRecord.length : -1));
logMessageContent(scanRecord);
handleDeviceFound(device, (short) rssi);
}
};
// why use a method to get callback?
// because this callback need API >= 21
// we cant add @TARGETAPI("Lollipop") at class header
// so use a method with SDK check to return this callback
private ScanCallback getScanCallback() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
newLeScanCallback = new ScanCallback() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
try {
ScanRecord scanRecord = result.getScanRecord();
ParcelUuid[] uuids = null;
if (scanRecord != null) {
//logMessageContent(scanRecord.getBytes());
List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
if (serviceUuids != null) {
uuids = serviceUuids.toArray(new ParcelUuid[0]);
}
}
LOG.warn(result.getDevice().getName() + ": " +
((scanRecord != null) ? scanRecord.getBytes().length : -1));
handleDeviceFound(result.getDevice(), (short) result.getRssi(), uuids);
} catch (NullPointerException e) {
LOG.warn("Error handling scan result", e);
}
}
};
}
return newLeScanCallback;
}
public void logMessageContent(byte[] value) {
if (value != null) {
for (byte b : value) {
LOG.warn("DATA: " + String.format("0x%2x", b) + " - " + (char) (b & 0xff));
}
}
}
private final Runnable stopRunnable = new Runnable() {
@Override
public void run() {
@ -107,6 +187,7 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
private enum Scanning {
SCANNING_BT,
SCANNING_BTLE,
SCANNING_NEW_BTLE,
SCANNING_OFF
}
@ -135,6 +216,7 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
IntentFilter bluetoothIntents = new IntentFilter();
bluetoothIntents.addAction(BluetoothDevice.ACTION_FOUND);
bluetoothIntents.addAction(BluetoothDevice.ACTION_UUID);
bluetoothIntents.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
@ -174,13 +256,43 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
@Override
protected void onDestroy() {
unregisterReceiver(bluetoothReceiver);
try {
unregisterReceiver(bluetoothReceiver);
} catch (IllegalArgumentException e) {
LOG.warn("Tried to unregister Bluetooth Receiver that wasn't registered.");
}
super.onDestroy();
}
private void handleDeviceFound(BluetoothDevice device, short rssi) {
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi);
if (DeviceHelper.getInstance().isSupported(candidate)) {
ParcelUuid[] uuids = device.getUuids();
if (uuids == null) {
if (device.fetchUuidsWithSdp()) {
return;
}
}
handleDeviceFound(device, rssi, uuids);
}
private void handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) {
LOG.debug("found device: " + device.getName() + ", " + device.getAddress());
if (LOG.isDebugEnabled()) {
if (uuids != null && uuids.length > 0) {
for (ParcelUuid uuid : uuids) {
LOG.debug(" supports uuid: " + uuid.toString());
}
}
}
if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
return; // ignore already bonded devices
}
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi, uuids);
DeviceType deviceType = DeviceHelper.getInstance().getSupportedType(candidate);
if (deviceType.isSupported()) {
candidate.setDeviceType(deviceType);
int index = deviceCandidates.indexOf(candidate);
if (index >= 0) {
deviceCandidates.set(index, candidate); // replace
@ -215,6 +327,12 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
} else {
discoveryFinished();
}
} else if (what == Scanning.SCANNING_NEW_BTLE) {
if (GB.supportsBluetoothLE()) {
startNEWBTLEDiscovery();
} else {
discoveryFinished();
}
}
} else {
discoveryFinished();
@ -238,6 +356,8 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
stopBTDiscovery();
} else if (wasScanning == Scanning.SCANNING_BTLE) {
stopBTLEDiscovery();
} else if (wasScanning == Scanning.SCANNING_NEW_BTLE) {
stopNewBTLEDiscovery();
}
handler.removeMessages(0, stopRunnable);
}
@ -251,6 +371,11 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
adapter.cancelDiscovery();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void stopNewBTLEDiscovery() {
adapter.getBluetoothLeScanner().stopScan(newLeScanCallback);
}
private void bluetoothStateChanged(int oldState, int newState) {
discoveryFinished();
if (newState == BluetoothAdapter.STATE_ON) {
@ -310,6 +435,41 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
return true;
}
// New BTLE Discovery use startScan (List<ScanFilter> filters,
// ScanSettings settings,
// ScanCallback callback)
// It's added on API21
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void startNEWBTLEDiscovery() {
// Only use new API when user uses Lollipop+ device
LOG.info("Start New BTLE Discovery");
handler.removeMessages(0, stopRunnable);
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
adapter.getBluetoothLeScanner().startScan(getScanFilters(), getScanSettings(), getScanCallback());
}
private List<ScanFilter> getScanFilters() {
List<ScanFilter> allFilters = new ArrayList<>();
for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) {
allFilters.addAll(coordinator.createBLEScanFilters());
}
return allFilters;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private ScanSettings getScanSettings() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return new ScanSettings.Builder()
.setScanMode(SCAN_MODE_LOW_LATENCY)
.setMatchMode(MATCH_MODE_STICKY)
.build();
} else {
return new ScanSettings.Builder()
.setScanMode(SCAN_MODE_LOW_LATENCY)
.build();
}
}
private void startBTLEDiscovery() {
LOG.info("Starting BTLE Discovery");
handler.removeMessages(0, stopRunnable);

View File

@ -1,5 +1,6 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@ -19,8 +20,14 @@ import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Scanner;
import java.util.UUID;
@ -28,6 +35,7 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
@ -37,7 +45,9 @@ public class ExternalPebbleJSActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class);
private UUID appUuid;
private Uri confUri;
private GBDevice mGBDevice = null;
private WebView myWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -46,23 +56,15 @@ public class ExternalPebbleJSActivity extends GBActivity {
Bundle extras = getIntent().getExtras();
if (extras != null) {
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
appUuid = (UUID) extras.getSerializable(DeviceService.EXTRA_APP_UUID);
} else {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
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 = (WebView) findViewById(R.id.configureWebview);
myWebView.clearCache(true);
myWebView.setWebViewClient(new GBWebClient());
myWebView.setWebChromeClient(new GBChromeClient());
@ -70,11 +72,39 @@ public class ExternalPebbleJSActivity extends GBActivity {
webSettings.setJavaScriptEnabled(true);
//needed to access the DOM
webSettings.setDomStorageEnabled(true);
//needed for localstorage
webSettings.setDatabaseEnabled(true);
JSInterface gbJSInterface = new JSInterface();
JSInterface gbJSInterface = new JSInterface(this);
myWebView.addJavascriptInterface(gbJSInterface, "GBjs");
myWebView.loadUrl("file:///android_asset/app_config/configure.html?" + queryString);
myWebView.loadUrl("file:///android_asset/app_config/configure.html");
}
@Override
protected void onNewIntent(Intent incoming) {
incoming.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
super.onNewIntent(incoming);
confUri = incoming.getData();
}
@Override
protected void onResume() {
super.onResume();
String queryString = "";
if (confUri != null) {
//getting back with configuration data
try {
appUuid = UUID.fromString(confUri.getHost());
queryString = confUri.getEncodedQuery();
} catch (IllegalArgumentException e) {
GB.toast("returned uri: " + confUri.toString(), Toast.LENGTH_LONG, GB.ERROR);
}
myWebView.loadUrl("file:///android_asset/app_config/configure.html?" + queryString);
}
}
private JSONObject getAppConfigurationKeys() {
@ -108,8 +138,8 @@ public class ExternalPebbleJSActivity extends GBActivity {
@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));
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
} else {
url = url.replaceFirst("^pebblejs://close#", "file:///android_asset/app_config/configure.html?config=true&json=");
@ -123,7 +153,10 @@ public class ExternalPebbleJSActivity extends GBActivity {
private class JSInterface {
public JSInterface() {
Context mContext;
public JSInterface(Context c) {
mContext = c;
}
@JavascriptInterface
@ -133,23 +166,22 @@ public class ExternalPebbleJSActivity extends GBActivity {
@JavascriptInterface
public void sendAppMessage(String msg) {
LOG.debug("from WEBVIEW: ", msg);
LOG.debug("from WEBVIEW: " + msg);
JSONObject knownKeys = getAppConfigurationKeys();
try {
JSONObject in = new JSONObject(msg);
JSONObject out = new JSONObject();
String inKey, outKey;
boolean passKey = false;
boolean passKey;
for (Iterator<String> key = in.keys(); key.hasNext(); ) {
passKey = false;
inKey = key.next();
outKey = null;
int pebbleAppIndex = knownKeys.optInt(inKey);
if (pebbleAppIndex != 0) {
int pebbleAppIndex = knownKeys.optInt(inKey, -1);
if (pebbleAppIndex != -1) {
passKey = true;
outKey = String.valueOf(pebbleAppIndex);
} else {
//do not discard integer keys (see https://developer.pebble.com/guides/communication/using-pebblekit-js/ )
Scanner scanner = new Scanner(inKey);
@ -159,7 +191,7 @@ public class ExternalPebbleJSActivity extends GBActivity {
}
}
if (passKey && outKey != null) {
if (passKey) {
Object obj = in.get(inKey);
if (obj instanceof Boolean) {
obj = ((Boolean) obj) ? "true" : "false";
@ -183,8 +215,8 @@ public class ExternalPebbleJSActivity extends GBActivity {
JSONObject wi = new JSONObject();
try {
wi.put("firmware", mGBDevice.getFirmwareVersion());
wi.put("platform", PebbleUtils.getPlatformName(mGBDevice.getHardwareVersion()));
wi.put("model", PebbleUtils.getModel(mGBDevice.getHardwareVersion()));
wi.put("platform", PebbleUtils.getPlatformName(mGBDevice.getModel()));
wi.put("model", PebbleUtils.getModel(mGBDevice.getModel()));
//TODO: use real info
wi.put("language", "en");
} catch (JSONException e) {
@ -208,16 +240,72 @@ public class ExternalPebbleJSActivity extends GBActivity {
return null;
}
@JavascriptInterface
public String getAppStoredPreset() {
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, appUuid.toString() + "_preset.json");
if (configurationFile.exists()) {
return FileUtils.getStringFromFile(configurationFile);
}
} catch (IOException e) {
GB.toast("Error reading presets", Toast.LENGTH_LONG, GB.ERROR);
e.printStackTrace();
}
return null;
}
@JavascriptInterface
public void saveAppStoredPreset(String msg) {
Writer writer;
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File presetsFile = new File(destDir, appUuid.toString() + "_preset.json");
writer = new BufferedWriter(new FileWriter(presetsFile));
writer.write(msg);
writer.close();
GB.toast("Presets stored", Toast.LENGTH_SHORT, GB.INFO);
} catch (IOException e) {
GB.toast("Error storing presets", Toast.LENGTH_LONG, GB.ERROR);
e.printStackTrace();
}
}
@JavascriptInterface
public String getAppUUID() {
return appUuid.toString();
}
@JavascriptInterface
public String getAppLocalstoragePrefix() {
String prefix = mGBDevice.getAddress() + appUuid.toString();
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = prefix.getBytes("UTF-8");
digest.update(bytes, 0, bytes.length);
bytes = digest.digest();
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
sb.append(String.format("%02X", bytes[i]));
}
return sb.toString().toLowerCase();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
e.printStackTrace();
return prefix;
}
}
@JavascriptInterface
public String getWatchToken() {
//specification says: A string that is is guaranteed to be identical for each Pebble device for the same app across different mobile devices. The token is unique to your app and cannot be used to track Pebble devices across applications. see https://developer.pebble.com/docs/js/Pebble/
//specification says: A string that is guaranteed to be identical for each Pebble device for the same app across different mobile devices. The token is unique to your app and cannot be used to track Pebble devices across applications. see https://developer.pebble.com/docs/js/Pebble/
return "gb" + appUuid.toString();
}
@JavascriptInterface
public void closeActivity() {
NavUtils.navigateUpFromSameTask((ExternalPebbleJSActivity) mContext);
}
}
@Override

View File

@ -0,0 +1,78 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.ProgressDialog;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.ActivityDatabaseHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class OnboardingActivity extends GBActivity {
private Button importOldActivityDataButton;
private TextView importOldActivityDataText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_onboarding);
Bundle extras = getIntent().getExtras();
GBDevice device;
if (extras != null) {
device = extras.getParcelable(GBDevice.EXTRA_DEVICE);
} else {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
importOldActivityDataText = (TextView) findViewById(R.id.textview_import_old_activitydata);
importOldActivityDataText.setText(String.format(getString(R.string.import_old_db_information), device.getName()));
importOldActivityDataButton = (Button) findViewById(R.id.button_import_old_activitydata);
final GBDevice finalDevice = device;
importOldActivityDataButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mergeOldActivityDbContents(finalDevice);
}
});
}
private void mergeOldActivityDbContents(final GBDevice device) {
if (device == null) {
return;
}
final DBHelper helper = new DBHelper(getBaseContext());
final ActivityDatabaseHandler oldHandler = helper.getOldActivityDatabaseHandler();
if (oldHandler == null) {
GB.toast(this, "No old activity database found, nothing to import.", Toast.LENGTH_LONG, GB.ERROR);
return;
}
try (DBHandler targetHandler = GBApplication.acquireDB()) {
final ProgressDialog progress = ProgressDialog.show(OnboardingActivity.this, "Merging Activity Data", "Please wait while merging your activity data...", true, false);
new AsyncTask<Object, ProgressDialog, Object>() {
@Override
protected Object doInBackground(Object[] params) {
helper.importOldDb(oldHandler, device, targetHandler);
progress.dismiss();
finish();
return null;
}
}.execute((Object[]) null);
} catch (Exception ex) {
GB.toast(OnboardingActivity.this, "Error importing old activity data into new database.", Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
}

View File

@ -7,6 +7,7 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.preference.EditTextPreference;
@ -21,14 +22,18 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_HEIGHT_CM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_SLEEP_DURATION;
@ -78,7 +83,7 @@ public class SettingsActivity extends AbstractSettingsActivity {
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
preference.setSummary(newVal.toString());
return true;
@ -90,7 +95,7 @@ public class SettingsActivity extends AbstractSettingsActivity {
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
preference.setSummary(newVal.toString());
return true;
@ -138,15 +143,32 @@ public class SettingsActivity extends AbstractSettingsActivity {
String provider = locationManager.getBestProvider(criteria, false);
if (provider != null) {
Location location = locationManager.getLastKnownLocation(provider);
String latitude = String.format(Locale.US, "%.6g", location.getLatitude());
String longitude = String.format(Locale.US, "%.6g", location.getLongitude());
LOG.info("got location. Lat: " + latitude + " Lng: " + longitude);
EditTextPreference pref_latitude = (EditTextPreference) findPreference("location_latitude");
EditTextPreference pref_longitude = (EditTextPreference) findPreference("location_longitude");
pref_latitude.setText(latitude);
pref_longitude.setText(longitude);
pref_latitude.setSummary(latitude);
pref_longitude.setSummary(longitude);
if (location != null) {
setLocationPreferences(location);
} else {
locationManager.requestSingleUpdate(provider, new LocationListener() {
@Override
public void onLocationChanged(Location location) {
setLocationPreferences(location);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
LOG.info("provider status changed to " + status + " (" + provider + ")");
}
@Override
public void onProviderEnabled(String provider) {
LOG.info("provider enabled (" + provider + ")");
}
@Override
public void onProviderDisabled(String provider) {
LOG.info("provider disabled (" + provider + ")");
GB.toast(SettingsActivity.this, getString(R.string.toast_enable_networklocationprovider), 3000, 0);
}
}, null);
}
} else {
LOG.warn("No location provider found, did you deny location permission?");
}
@ -154,6 +176,25 @@ public class SettingsActivity extends AbstractSettingsActivity {
}
});
pref = findPreference("canned_messages_dismisscall_send");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Prefs prefs = GBApplication.getPrefs();
ArrayList<String> messages = new ArrayList<>();
for (int i = 1; i <= 16; i++) {
String message = prefs.getString("canned_message_dismisscall_" + i, null);
if (message != null && !message.equals("")) {
messages.add(message);
}
}
CannedMessagesSpec cannedMessagesSpec = new CannedMessagesSpec();
cannedMessagesSpec.type = CannedMessagesSpec.TYPE_MISSEDCALLS;
cannedMessagesSpec.cannedMessages = messages.toArray(new String[messages.size()]);
GBApplication.deviceService().onSetCannedMessages(cannedMessagesSpec);
return true;
}
});
// Get all receivers of Media Buttons
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
@ -205,6 +246,22 @@ public class SettingsActivity extends AbstractSettingsActivity {
"canned_reply_14",
"canned_reply_15",
"canned_reply_16",
"canned_message_dismisscall_1",
"canned_message_dismisscall_2",
"canned_message_dismisscall_3",
"canned_message_dismisscall_4",
"canned_message_dismisscall_5",
"canned_message_dismisscall_6",
"canned_message_dismisscall_7",
"canned_message_dismisscall_8",
"canned_message_dismisscall_9",
"canned_message_dismisscall_10",
"canned_message_dismisscall_11",
"canned_message_dismisscall_12",
"canned_message_dismisscall_13",
"canned_message_dismisscall_14",
"canned_message_dismisscall_15",
"canned_message_dismisscall_16",
PREF_USER_YEAR_OF_BIRTH,
PREF_USER_HEIGHT_CM,
PREF_USER_WEIGHT_KG,
@ -212,4 +269,16 @@ public class SettingsActivity extends AbstractSettingsActivity {
};
}
private void setLocationPreferences(Location location) {
String latitude = String.format(Locale.US, "%.6g", location.getLatitude());
String longitude = String.format(Locale.US, "%.6g", location.getLongitude());
LOG.info("got location. Lat: " + latitude + " Lng: " + longitude);
GB.toast(SettingsActivity.this, getString(R.string.toast_aqurired_networklocation), 2000, 0);
EditTextPreference pref_latitude = (EditTextPreference) findPreference("location_latitude");
EditTextPreference pref_longitude = (EditTextPreference) findPreference("location_longitude");
pref_latitude.setText(latitude);
pref_longitude.setText(longitude);
pref_latitude.setSummary(latitude);
pref_longitude.setSummary(longitude);
}
}

View File

@ -0,0 +1,71 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.SeekBar;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
public class VibrationActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(VibrationActivity.class);
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case GBApplication.ACTION_QUIT: {
finish();
break;
}
}
}
};
private SeekBar seekBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_vibration);
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
registerReceiver(mReceiver, filter);
seekBar = (SeekBar) findViewById(R.id.vibration_seekbar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (progress > 0) { // 1-16
progress = progress * 16 - 1; // max 255
}
GBApplication.deviceService().onSetConstantVibration(progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
unregisterReceiver(mReceiver);
}
}

View File

@ -0,0 +1,398 @@
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.widget.LinearLayoutManager;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import com.woxthebox.draglistview.DragListView;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
public abstract class AbstractAppManagerFragment extends Fragment {
public static final String ACTION_REFRESH_APPLIST
= "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist";
private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
protected abstract List<GBDeviceApp> getSystemAppsInCategory();
protected abstract String getSortFilename();
protected abstract boolean isCacheManager();
protected abstract boolean filterApp(GBDeviceApp gbDeviceApp);
protected void onChangedAppOrder() {
List<UUID> uuidList = new ArrayList<>();
for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getItemList()) {
uuidList.add(gbDeviceApp.getUUID());
}
AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuidList);
}
protected void refreshList() {
appList.clear();
ArrayList uuids = AppManagerActivity.getUuidsFromFile(getSortFilename());
List<GBDeviceApp> systemApps = getSystemAppsInCategory();
boolean needsRewrite = false;
for (GBDeviceApp systemApp : systemApps) {
if (!uuids.contains(systemApp.getUUID())) {
uuids.add(systemApp.getUUID());
needsRewrite = true;
}
}
if (needsRewrite) {
AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuids);
}
appList.addAll(getCachedApps(uuids));
}
private void refreshListFromPebble(Intent intent) {
appList.clear();
int appCount = intent.getIntExtra("app_count", 0);
for (Integer i = 0; i < appCount; i++) {
String appName = intent.getStringExtra("app_name" + i.toString());
String appCreator = intent.getStringExtra("app_creator" + i.toString());
UUID uuid = UUID.fromString(intent.getStringExtra("app_uuid" + i.toString()));
GBDeviceApp.Type appType = GBDeviceApp.Type.values()[intent.getIntExtra("app_type" + i.toString(), 0)];
GBDeviceApp app = new GBDeviceApp(uuid, appName, appCreator, "", appType);
app.setOnDevice(true);
if (filterApp(app)) {
appList.add(app);
}
}
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(ACTION_REFRESH_APPLIST)) {
if (intent.hasExtra("app_count")) {
LOG.info("got app info from pebble");
if (!isCacheManager()) {
LOG.info("will refresh list based on data from pebble");
refreshListFromPebble(intent);
}
} else if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 3 || isCacheManager()) {
refreshList();
}
mGBDeviceAppAdapter.notifyDataSetChanged();
}
}
};
private DragListView appListView;
protected final List<GBDeviceApp> appList = new ArrayList<>();
private GBDeviceAppAdapter mGBDeviceAppAdapter;
protected GBDevice mGBDevice = null;
protected List<GBDeviceApp> getCachedApps(List<UUID> uuids) {
List<GBDeviceApp> cachedAppList = new ArrayList<>();
File cachePath;
try {
cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache");
} catch (IOException e) {
LOG.warn("could not get external dir while reading pbw cache.");
return cachedAppList;
}
File[] files;
if (uuids == null) {
files = cachePath.listFiles();
} else {
files = new File[uuids.size()];
int index = 0;
for (UUID uuid : uuids) {
files[index++] = new File(uuid.toString() + ".pbw");
}
}
if (files != null) {
for (File file : files) {
if (file.getName().endsWith(".pbw")) {
String baseName = file.getName().substring(0, file.getName().length() - 4);
//metadata
File jsonFile = new File(cachePath, baseName + ".json");
//configuration
File configFile = new File(cachePath, baseName + "_config.js");
try {
String jsonstring = FileUtils.getStringFromFile(jsonFile);
JSONObject json = new JSONObject(jsonstring);
cachedAppList.add(new GBDeviceApp(json, configFile.exists()));
} catch (Exception e) {
LOG.info("could not read json file for " + baseName);
//FIXME: this is really ugly, if we do not find system uuids in pbw cache add them manually. Also duplicated code
switch (baseName) {
case "8f3c8686-31a1-4f5f-91f5-01600c9bdc59":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Tic Toc (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
break;
case "1f03293d-47af-4f28-b960-f2b02a6dd757":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Music (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
break;
case "b2cae818-10f8-46df-ad2b-98ad2254a3c1":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
break;
case "67a32d95-ef69-46d4-a0b9-854cc62f97f9":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Alarms (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
break;
case "18e443ce-38fd-47c8-84d5-6d0c775fbe55":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Watchfaces (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
break;
case "0863fc6a-66c5-4f62-ab8a-82ed00a98b5d":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Send Text (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
break;
}
/*
else if (baseName.equals("4dab81a6-d2fc-458a-992c-7a1f3b96a970")) {
cachedAppList.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
} else if (baseName.equals("cf1e816a-9db0-4511-bbb8-f60c48ca8fac")) {
cachedAppList.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
*/
if (mGBDevice != null) {
if (PebbleUtils.hasHealth(mGBDevice.getModel())) {
if (baseName.equals(PebbleProtocol.UUID_PEBBLE_HEALTH.toString())) {
cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
continue;
}
}
if (PebbleUtils.hasHRM(mGBDevice.getModel())) {
if (baseName.equals(PebbleProtocol.UUID_WORKOUT.toString())) {
cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_WORKOUT, "Workout (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
continue;
}
}
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 4) {
if (baseName.equals("3af858c3-16cb-4561-91e7-f1ad2df8725f")) {
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Kickstart (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
}
}
}
if (uuids == null) {
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), baseName, "N/A", "", GBDeviceApp.Type.UNKNOWN));
}
}
}
}
}
return cachedAppList;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mGBDevice = ((AppManagerActivity) getActivity()).getGBDevice();
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) < 3 && !isCacheManager()) {
appListView.setDragEnabled(false);
}
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_REFRESH_APPLIST);
LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver, filter);
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) < 3) {
GBApplication.deviceService().onAppInfoReq();
if (isCacheManager()) {
refreshList();
}
} else {
refreshList();
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.activity_appmanager, container, false);
appListView = (DragListView) (rootView.findViewById(R.id.appListView));
appListView.setLayoutManager(new LinearLayoutManager(getActivity()));
mGBDeviceAppAdapter = new GBDeviceAppAdapter(appList, R.layout.item_with_details, R.id.item_image, this.getContext(), this);
appListView.setAdapter(mGBDeviceAppAdapter, false);
appListView.setCanDragHorizontally(false);
appListView.setDragListListener(new DragListView.DragListListener() {
@Override
public void onItemDragStarted(int position) {
}
@Override
public void onItemDragging(int itemPosition, float x, float y) {
}
@Override
public void onItemDragEnded(int fromPosition, int toPosition) {
onChangedAppOrder();
}
});
return rootView;
}
protected void sendOrderToDevice(String concatFilename) {
ArrayList<UUID> uuids = new ArrayList<>();
for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getItemList()) {
uuids.add(gbDeviceApp.getUUID());
}
if (concatFilename != null) {
ArrayList<UUID> concatUuids = AppManagerActivity.getUuidsFromFile(concatFilename);
uuids.addAll(concatUuids);
}
GBApplication.deviceService().onAppReorder(uuids.toArray(new UUID[uuids.size()]));
}
public boolean openPopupMenu(View view, int position) {
PopupMenu popupMenu = new PopupMenu(getContext(), view);
popupMenu.getMenuInflater().inflate(R.menu.appmanager_context, popupMenu.getMenu());
Menu menu = popupMenu.getMenu();
final GBDeviceApp selectedApp = appList.get(position);
if (!selectedApp.isInCache()) {
menu.removeItem(R.id.appmanager_app_reinstall);
menu.removeItem(R.id.appmanager_app_delete_cache);
}
if (!PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) {
menu.removeItem(R.id.appmanager_health_activate);
menu.removeItem(R.id.appmanager_health_deactivate);
}
if (!PebbleProtocol.UUID_WORKOUT.equals(selectedApp.getUUID())) {
menu.removeItem(R.id.appmanager_hrm_activate);
menu.removeItem(R.id.appmanager_hrm_deactivate);
}
if (selectedApp.getType() == GBDeviceApp.Type.APP_SYSTEM || selectedApp.getType() == GBDeviceApp.Type.WATCHFACE_SYSTEM) {
menu.removeItem(R.id.appmanager_app_delete);
}
if (!selectedApp.isConfigurable()) {
menu.removeItem(R.id.appmanager_app_configure);
}
switch (selectedApp.getType()) {
case WATCHFACE:
case APP_GENERIC:
case APP_ACTIVITYTRACKER:
break;
default:
menu.removeItem(R.id.appmanager_app_openinstore);
}
//menu.setHeaderTitle(selectedApp.getName());
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
return onContextItemSelected(item, selectedApp);
}
}
);
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
popupMenu.show();
return true;
}
public boolean onContextItemSelected(MenuItem item, GBDeviceApp selectedApp) {
switch (item.getItemId()) {
case R.id.appmanager_app_delete_cache:
String baseName;
try {
baseName = FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID();
} catch (IOException e) {
LOG.warn("could not get external dir while trying to access pbw cache.");
return true;
}
String[] suffixToDelete = new String[]{".pbw", ".json", "_config.js", "_preset.json"};
for (String suffix : suffixToDelete) {
File fileToDelete = new File(baseName + suffix);
if (!fileToDelete.delete()) {
LOG.warn("could not delete file from pbw cache: " + fileToDelete.toString());
} else {
LOG.info("deleted file: " + fileToDelete.toString());
}
}
AppManagerActivity.deleteFromAppOrderFile("pbwcacheorder.txt", selectedApp.getUUID()); // FIXME: only if successful
// fall through
case R.id.appmanager_app_delete:
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 3) {
AppManagerActivity.deleteFromAppOrderFile(mGBDevice.getAddress() + ".watchapps", selectedApp.getUUID()); // FIXME: only if successful
AppManagerActivity.deleteFromAppOrderFile(mGBDevice.getAddress() + ".watchfaces", selectedApp.getUUID()); // FIXME: only if successful
Intent refreshIntent = new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(refreshIntent);
}
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
return true;
case R.id.appmanager_app_reinstall:
File cachePath;
try {
cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID() + ".pbw");
} catch (IOException e) {
LOG.warn("could not get external dir while trying to access pbw cache.");
return true;
}
GBApplication.deviceService().onInstallApp(Uri.fromFile(cachePath));
return true;
case R.id.appmanager_health_activate:
GBApplication.deviceService().onInstallApp(Uri.parse("fake://health"));
return true;
case R.id.appmanager_hrm_activate:
GBApplication.deviceService().onInstallApp(Uri.parse("fake://hrm"));
return true;
case R.id.appmanager_health_deactivate:
case R.id.appmanager_hrm_deactivate:
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
return true;
case R.id.appmanager_app_configure:
GBApplication.deviceService().onAppStart(selectedApp.getUUID(), true);
Intent startIntent = new Intent(getContext().getApplicationContext(), ExternalPebbleJSActivity.class);
startIntent.putExtra(DeviceService.EXTRA_APP_UUID, selectedApp.getUUID());
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
startActivity(startIntent);
return true;
case R.id.appmanager_app_openinstore:
String url = "https://apps.getpebble.com/en_US/search/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/1?query=" + selectedApp.getName() + "&dev_settings=true";
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
startActivity(intent);
return true;
default:
return super.onContextItemSelected(item);
}
}
@Override
public void onDestroy() {
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -0,0 +1,187 @@
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewPager;
import android.view.MenuItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public class AppManagerActivity extends AbstractGBFragmentActivity {
private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
private GBDevice mGBDevice = null;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_QUIT:
finish();
break;
}
}
};
public GBDevice getGBDevice() {
return mGBDevice;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragmentappmanager);
Bundle extras = getIntent().getExtras();
if (extras != null) {
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
} else {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
// Set up the ViewPager with the sections adapter.
ViewPager viewPager = (ViewPager) findViewById(R.id.appmanager_pager);
if (viewPager != null) {
viewPager.setAdapter(getPagerAdapter());
}
}
@Override
protected AbstractFragmentPagerAdapter createFragmentPagerAdapter(FragmentManager fragmentManager) {
return new SectionsPagerAdapter(fragmentManager);
}
public static synchronized void deleteFromAppOrderFile(String filename, UUID uuid) {
ArrayList<UUID> uuids = getUuidsFromFile(filename);
uuids.remove(uuid);
rewriteAppOrderFile(filename, uuids);
}
public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
switch (position) {
case 0:
return new AppManagerFragmentCache();
case 1:
return new AppManagerFragmentInstalledApps();
case 2:
return new AppManagerFragmentInstalledWatchfaces();
}
return null;
}
@Override
public int getCount() {
return 3;
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.appmanager_cached_watchapps_watchfaces);
case 1:
return getString(R.string.appmanager_installed_watchapps);
case 2:
return getString(R.string.appmanager_installed_watchfaces);
case 3:
}
return super.getPageTitle(position);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
static synchronized void rewriteAppOrderFile(String filename, List<UUID> uuids) {
try {
FileWriter fileWriter = new FileWriter(FileUtils.getExternalFilesDir() + "/" + filename);
BufferedWriter out = new BufferedWriter(fileWriter);
for (UUID uuid : uuids) {
out.write(uuid.toString());
out.newLine();
}
out.close();
} catch (IOException e) {
LOG.warn("can't write app order to file!");
}
}
synchronized public static void addToAppOrderFile(String filename, UUID uuid) {
ArrayList<UUID> uuids = getUuidsFromFile(filename);
if (!uuids.contains(uuid)) {
uuids.add(uuid);
rewriteAppOrderFile(filename, uuids);
}
}
static synchronized ArrayList<UUID> getUuidsFromFile(String filename) {
ArrayList<UUID> uuids = new ArrayList<>();
try {
FileReader fileReader = new FileReader(FileUtils.getExternalFilesDir() + "/" + filename);
BufferedReader in = new BufferedReader(fileReader);
String line;
while ((line = in.readLine()) != null) {
uuids.add(UUID.fromString(line));
}
} catch (IOException e) {
LOG.warn("could not read sort file");
}
return uuids;
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
public class AppManagerFragmentCache extends AbstractAppManagerFragment {
@Override
public void refreshList() {
appList.clear();
appList.addAll(getCachedApps(null));
}
@Override
protected boolean isCacheManager() {
return true;
}
@Override
protected List<GBDeviceApp> getSystemAppsInCategory() {
return null;
}
@Override
public String getSortFilename() {
return "pbwcacheorder.txt";
}
@Override
protected boolean filterApp(GBDeviceApp gbDeviceApp) {
return true;
}
}

View File

@ -0,0 +1,56 @@
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
public class AppManagerFragmentInstalledApps extends AbstractAppManagerFragment {
@Override
protected List<GBDeviceApp> getSystemAppsInCategory() {
List<GBDeviceApp> systemApps = new ArrayList<>();
//systemApps.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
//systemApps.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("1f03293d-47af-4f28-b960-f2b02a6dd757"), "Music (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("b2cae818-10f8-46df-ad2b-98ad2254a3c1"), "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("67a32d95-ef69-46d4-a0b9-854cc62f97f9"), "Alarms (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("18e443ce-38fd-47c8-84d5-6d0c775fbe55"), "Watchfaces (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
if (mGBDevice != null) {
if (PebbleUtils.hasHealth(mGBDevice.getModel())) {
systemApps.add(new GBDeviceApp(UUID.fromString("0863fc6a-66c5-4f62-ab8a-82ed00a98b5d"), "Send Text (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
if (PebbleUtils.hasHRM(mGBDevice.getModel())) {
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_WORKOUT, "Workout (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
}
return systemApps;
}
@Override
protected boolean isCacheManager() {
return false;
}
@Override
protected String getSortFilename() {
return mGBDevice.getAddress() + ".watchapps";
}
@Override
protected void onChangedAppOrder() {
super.onChangedAppOrder();
sendOrderToDevice(mGBDevice.getAddress() + ".watchfaces");
}
@Override
protected boolean filterApp(GBDeviceApp gbDeviceApp) {
return gbDeviceApp.getType() == GBDeviceApp.Type.APP_ACTIVITYTRACKER || gbDeviceApp.getType() == GBDeviceApp.Type.APP_GENERIC;
}
}

View File

@ -0,0 +1,42 @@
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
public class AppManagerFragmentInstalledWatchfaces extends AbstractAppManagerFragment {
@Override
protected List<GBDeviceApp> getSystemAppsInCategory() {
List<GBDeviceApp> systemWatchfaces = new ArrayList<>();
systemWatchfaces.add(new GBDeviceApp(UUID.fromString("8f3c8686-31a1-4f5f-91f5-01600c9bdc59"), "Tic Toc (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
systemWatchfaces.add(new GBDeviceApp(UUID.fromString("3af858c3-16cb-4561-91e7-f1ad2df8725f"), "Kickstart (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
return systemWatchfaces;
}
@Override
protected boolean isCacheManager() {
return false;
}
@Override
protected String getSortFilename() {
return mGBDevice.getAddress() + ".watchfaces";
}
@Override
protected void onChangedAppOrder() {
super.onChangedAppOrder();
sendOrderToDevice(mGBDevice.getAddress() + ".watchapps");
}
@Override
protected boolean filterApp(GBDeviceApp gbDeviceApp) {
if (gbDeviceApp.getType() == GBDeviceApp.Type.WATCHFACE) {
return true;
}
return false;
}
}

View File

@ -8,19 +8,25 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.TypedValue;
import android.view.View;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.data.ChartData;
import com.github.mikephil.charting.data.CombinedData;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
import org.slf4j.Logger;
@ -30,7 +36,6 @@ 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;
@ -45,6 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@ -82,17 +88,18 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
};
private boolean mChartDirty = true;
private boolean supportsHeartrateChart = true;
private AsyncTask refreshTask;
public boolean isChartDirty() {
return mChartDirty;
}
@Override
public abstract String getTitle();
public boolean supportsHeartrate() {
return supportsHeartrateChart;
public boolean supportsHeartrate(GBDevice device) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
return coordinator != null && coordinator.supportsHeartRateMeasurement(device);
}
protected static final class ActivityConfig {
@ -150,15 +157,20 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
protected void init() {
TypedValue runningColor = new TypedValue();
BACKGROUND_COLOR = GBApplication.getBackgroundColor(getContext());
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(getContext());
CHART_TEXT_COLOR = getResources().getColor(R.color.secondarytext);
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);
CHART_TEXT_COLOR = ContextCompat.getColor(getContext(), R.color.secondarytext);
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
HEARTRATE_FILL_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_fill);
getContext().getTheme().resolveAttribute(R.attr.chart_activity, runningColor, true);
AK_ACTIVITY_COLOR = runningColor.data;
getContext().getTheme().resolveAttribute(R.attr.chart_light_sleep, runningColor, true);
AK_DEEP_SLEEP_COLOR = runningColor.data;
getContext().getTheme().resolveAttribute(R.attr.chart_deep_sleep, runningColor, true);
AK_LIGHT_SLEEP_COLOR = runningColor.data;
getContext().getTheme().resolveAttribute(R.attr.chart_not_worn, runningColor, true);
AK_NOT_WORN_COLOR = runningColor.data;
HEARTRATE_LABEL = getContext().getString(R.string.charts_legend_heartrate);
@ -292,9 +304,9 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
return akActivity.color;
}
protected SampleProvider getProvider(GBDevice device) {
protected SampleProvider<? extends AbstractActivitySample> getProvider(DBHandler db, GBDevice device) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
return coordinator.getSampleProvider();
return coordinator.getSampleProvider(device, db.getDaoSession());
}
/**
@ -305,40 +317,25 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
* @param tsFrom
* @param tsTo
*/
protected List<ActivitySample> getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider provider = getProvider(device);
return db.getAllActivitySamples(tsFrom, tsTo, provider);
protected List<? extends ActivitySample> getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
return provider.getAllActivitySamples(tsFrom, tsTo);
}
private int getTSLast24Hours(int tsTo) {
return (tsTo) - (24 * 60 * 60); // -24 hours
}
protected List<ActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider provider = getProvider(device);
return db.getActivitySamples(tsFrom, tsTo, provider);
protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider<? extends AbstractActivitySample> provider = getProvider(db, device);
return provider.getActivitySamples(tsFrom, tsTo);
}
protected List<ActivitySample> getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider provider = getProvider(device);
return db.getSleepSamples(tsFrom, tsTo, provider);
}
protected List<ActivitySample> getTestSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(2015, Calendar.JUNE, 10, 6, 40);
// ignore provided date ranges
tsTo = (int) ((cal.getTimeInMillis() / 1000));
tsFrom = tsTo - (24 * 60 * 60);
SampleProvider provider = getProvider(device);
return db.getAllActivitySamples(tsFrom, tsTo, provider);
protected List<? extends ActivitySample> getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
return provider.getSleepSamples(tsFrom, tsTo);
}
protected void configureChartDefaults(Chart<?> chart) {
chart.setDescription("");
chart.getXAxis().setValueFormatter(new TimestampValueFormatter());
chart.getDescription().setText("");
// if enabled, the chart will always start at zero on the y-axis
chart.setNoDataText(getString(R.string.chart_no_data_synchronize));
@ -349,11 +346,19 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
// enable touch gestures
chart.setTouchEnabled(true);
// commented out: this has weird bugs/sideeffects at least on WeekStepsCharts
// where only the first Day-label is drawn, because AxisRenderer.computeAxisValues(float,float)
// appears to have an overflow when calculating 'n' (number of entries)
// chart.getXAxis().setGranularity(60*5);
setupLegend(chart);
}
protected void configureBarLineChartDefaults(BarLineChartBase<?> chart) {
configureChartDefaults(chart);
if (chart instanceof BarChart) {
((BarChart) chart).setFitBars(true);
}
// enable scaling and dragging
chart.setDragEnabled(true);
@ -397,12 +402,14 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
*/
protected abstract void renderCharts();
protected DefaultChartsData refresh(GBDevice gbDevice, List<ActivitySample> samples) {
Calendar cal = GregorianCalendar.getInstance();
cal.clear();
Date date;
String dateStringFrom = "";
String dateStringTo = "";
protected DefaultChartsData<CombinedData> refresh(GBDevice gbDevice, List<? extends ActivitySample> samples) {
// Calendar cal = GregorianCalendar.getInstance();
// cal.clear();
TimestampTranslation tsTranslation = new TimestampTranslation();
// Date date;
// String dateStringFrom = "";
// String dateStringTo = "";
// ArrayList<String> xLabels = null;
LOG.info("" + getTitle() + ": number of samples:" + samples.size());
CombinedData combinedData;
@ -412,13 +419,9 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
int last_type = ActivityKind.TYPE_UNKNOWN;
SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
int numEntries = samples.size();
List<String> xLabels = new ArrayList<>(numEntries);
List<BarEntry> activityEntries = new ArrayList<>(numEntries);
boolean hr = supportsHeartrate();
boolean hr = supportsHeartrate(gbDevice);
List<Entry> heartrateEntries = hr ? new ArrayList<Entry>(numEntries) : null;
List<Integer> colors = new ArrayList<>(numEntries); // this is kinda inefficient...
int lastHrSampleIndex = -1;
@ -426,17 +429,20 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
for (int i = 0; i < numEntries; i++) {
ActivitySample sample = samples.get(i);
int type = sample.getKind();
int ts = tsTranslation.shorten(sample.getTimestamp());
// System.out.println(ts);
// ts = i;
// determine start and end dates
if (i == 0) {
cal.setTimeInMillis(sample.getTimestamp() * 1000L); // make sure it's converted to long
date = cal.getTime();
dateStringFrom = dateFormat.format(date);
} else if (i == samples.size() - 1) {
cal.setTimeInMillis(sample.getTimestamp() * 1000L); // same here
date = cal.getTime();
dateStringTo = dateFormat.format(date);
}
// if (i == 0) {
// cal.setTimeInMillis(ts * 1000L); // make sure it's converted to long
// date = cal.getTime();
// dateStringFrom = dateFormat.format(date);
// } else if (i == samples.size() - 1) {
// cal.setTimeInMillis(ts * 1000L); // same here
// date = cal.getTime();
// dateStringTo = dateFormat.format(date);
// }
float movement = sample.getIntensity();
@ -462,23 +468,23 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
// value = ((float) movement) / movement_divisor;
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) {
activityEntries.add(createBarEntry(value, ts));
if (hr && isValidHeartRateValue(sample.getHeartRate())) {
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 60*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1));
heartrateEntries.add(createLineEntry(0, i - 1));
heartrateEntries.add(createLineEntry(0, ts - 1));
}
heartrateEntries.add(createLineEntry(sample.getCustomValue(), i));
lastHrSampleIndex = i;
heartrateEntries.add(createLineEntry(sample.getHeartRate(), ts));
lastHrSampleIndex = ts;
}
String xLabel = "";
if (annotate) {
cal.setTimeInMillis(sample.getTimestamp() * 1000L);
date = cal.getTime();
String dateString = annotationDateFormat.format(date);
xLabel = dateString;
// cal.setTimeInMillis((ts + tsOffset) * 1000L);
// date = cal.getTime();
// String dateString = annotationDateFormat.format(date);
// xLabel = dateString;
// if (last_type != type) {
// if (isSleep(last_type) && !isSleep(type)) {
// // woken up
@ -496,35 +502,35 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
// chart.getXAxis().addLimitLine(line);
// }
// }
last_type = type;
// last_type = type;
}
xLabels.add(xLabel);
}
// chart.getXAxis().setValues(xLabels);
BarDataSet activitySet = createActivitySet(activityEntries, colors, "Activity");
// create a data object with the datasets
combinedData = new CombinedData(xLabels);
// combinedData = new CombinedData(xLabels);
combinedData = new CombinedData();
List<IBarDataSet> list = new ArrayList<>();
list.add(activitySet);
BarData barData = new BarData(xLabels, list);
barData.setGroupSpace(0);
BarData barData = new BarData(list);
barData.setBarWidth(100f);
// barData.setGroupSpace(0);
combinedData.setData(barData);
if (hr && heartrateEntries.size() > 0) {
LineDataSet heartrateSet = createHeartrateSet(heartrateEntries, "Heart Rate");
LineData lineData = new LineData(xLabels, heartrateSet);
LineData lineData = new LineData(heartrateSet);
combinedData.setData(lineData);
}
// chart.setDescription(getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo));
// chart.setDescriptionPosition(?, ?);
} else {
combinedData = new CombinedData(Collections.<String>emptyList());
combinedData = new CombinedData();
}
return new DefaultChartsData(combinedData);
IAxisValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
return new DefaultChartsData(combinedData, xValueFormatter);
}
protected boolean isValidHeartRateValue(int value) {
@ -540,16 +546,16 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
* @param tsTo
* @return
*/
protected abstract List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo);
protected abstract List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo);
protected abstract void setupLegend(Chart chart);
protected BarEntry createBarEntry(float value, int index) {
return new BarEntry(value, index);
protected BarEntry createBarEntry(float value, int xValue) {
return new BarEntry(xValue, value);
}
protected Entry createLineEntry(float value, int index) {
return new Entry(value, index);
protected Entry createLineEntry(float value, int xValue) {
return new Entry(xValue, value);
}
protected BarDataSet createActivitySet(List<BarEntry> values, List<Integer> colors, String label) {
@ -574,7 +580,8 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
LineDataSet set1 = new LineDataSet(values, label);
set1.setLineWidth(0.8f);
set1.setColor(HEARTRATE_COLOR);
set1.setDrawCubic(true);
// set1.setDrawCubic(true);
set1.setMode(LineDataSet.Mode.CUBIC_BEZIER);
set1.setCubicIntensity(0.1f);
set1.setDrawCircles(false);
// set1.setCircleRadius(2f);
@ -688,8 +695,46 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
}
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device) {
return getSamples(db, device, getTSStart(), getTSEnd());
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device) {
int tsStart = getTSStart();
int tsEnd = getTSEnd();
List<ActivitySample> samples = (List<ActivitySample>) getSamples(db, device, tsStart, tsEnd);
ensureStartAndEndSamples(samples, tsStart, tsEnd);
// List<ActivitySample> samples2 = new ArrayList<>();
// int min = Math.min(samples.size(), 10);
// int min = Math.min(samples.size(), 10);
// for (int i = 0; i < min; i++) {
// samples2.add(samples.get(i));
// }
// return samples2;
return samples;
}
protected void ensureStartAndEndSamples(List<ActivitySample> samples, int tsStart, int tsEnd) {
if (samples == null || samples.isEmpty()) {
return;
}
ActivitySample lastSample = samples.get(samples.size() - 1);
if (lastSample.getTimestamp() < tsEnd) {
samples.add(createTrailingActivitySample(lastSample, tsEnd));
}
ActivitySample firstSample = samples.get(0);
if (firstSample.getTimestamp() > tsStart) {
samples.add(createTrailingActivitySample(firstSample, tsStart));
}
}
private ActivitySample createTrailingActivitySample(ActivitySample referenceSample, int timestamp) {
TrailingActivitySample sample = new TrailingActivitySample();
if (referenceSample instanceof AbstractActivitySample) {
AbstractActivitySample reference = (AbstractActivitySample) referenceSample;
sample.setUserId(reference.getUserId());
sample.setDeviceId(reference.getDeviceId());
sample.setProvider(reference.getProvider());
}
sample.setTimestamp(timestamp);
return sample;
}
private int getTSEnd() {
@ -704,15 +749,87 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
return (int) ((date.getTime() / 1000));
}
public static class DefaultChartsData extends ChartsData {
private final CombinedData combinedData;
public static class DefaultChartsData<T extends ChartData<?>> extends ChartsData {
private final T data;
private IAxisValueFormatter xValueFormatter;
public DefaultChartsData(CombinedData combinedData) {
this.combinedData = combinedData;
public DefaultChartsData(T data, IAxisValueFormatter xValueFormatter) {
this.xValueFormatter = xValueFormatter;
this.data = data;
}
public CombinedData getCombinedData() {
return combinedData;
public IAxisValueFormatter getXValueFormatter() {
return xValueFormatter;
}
public T getData() {
return data;
}
}
protected static class SampleXLabelFormatter implements IAxisValueFormatter {
private final TimestampTranslation tsTranslation;
SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
// SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
Calendar cal = GregorianCalendar.getInstance();
public SampleXLabelFormatter(TimestampTranslation tsTranslation) {
this.tsTranslation = tsTranslation;
}
// TODO: this does not work. Cannot use precomputed labels
@Override
public String getFormattedValue(float value, AxisBase axis) {
cal.clear();
int ts = (int) value;
cal.setTimeInMillis(tsTranslation.toOriginalValue(ts) * 1000L);
Date date = cal.getTime();
String dateString = annotationDateFormat.format(date);
return dateString;
}
}
protected static class PreformattedXIndexLabelFormatter implements IAxisValueFormatter {
private ArrayList<String> xLabels;
public PreformattedXIndexLabelFormatter(ArrayList<String> xLabels) {
this.xLabels = xLabels;
}
@Override
public String getFormattedValue(float value, AxisBase axis) {
int index = (int) value;
if (xLabels == null || index >= xLabels.size()) {
return String.valueOf(value);
}
return xLabels.get(index);
}
}
/**
* Awkward class that helps in translating long timestamp
* values to float (sic!) values. It basically rebases all
* timestamps to a base (the very first) timestamp value.
*
* It does this so that the large timestamp values can be used
* floating point values, where the mantissa is just 24 bits.
*/
protected static class TimestampTranslation {
private int tsOffset = -1;
public int shorten(int timestamp) {
if (tsOffset == -1) {
tsOffset = timestamp;
return 0;
}
return timestamp - tsOffset;
}
public int toOriginalValue(int timestamp) {
if (tsOffset == -1) {
return timestamp;
}
return timestamp + tsOffset;
}
}
}

View File

@ -8,7 +8,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class ActivityAnalysis {
public ActivityAmounts calculateActivityAmounts(List<ActivitySample> samples) {
public ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> samples) {
ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP);
ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP);
ActivityAmount notWorn = new ActivityAmount(ActivityKind.TYPE_NOT_WORN);
@ -17,7 +17,7 @@ public class ActivityAnalysis {
ActivityAmount previousAmount = null;
ActivitySample previousSample = null;
for (ActivitySample sample : samples) {
ActivityAmount amount = null;
ActivityAmount amount;
switch (sample.getKind()) {
case ActivityKind.TYPE_DEEP_SLEEP:
amount = deepSleep;
@ -43,8 +43,6 @@ public class ActivityAnalysis {
previousAmount.addSeconds(sharedTimeDifference);
amount.addSeconds(sharedTimeDifference);
}
} else {
// nothing to do, we can only calculate when we have the next sample
}
previousAmount = amount;
@ -66,10 +64,13 @@ public class ActivityAnalysis {
return result;
}
public int calculateTotalSteps(List<ActivitySample> samples) {
public int calculateTotalSteps(List<? extends ActivitySample> samples) {
int totalSteps = 0;
for (ActivitySample sample : samples) {
totalSteps += sample.getSteps();
int steps = sample.getSteps();
if (steps > 0) {
totalSteps += sample.getSteps();
}
}
return totalSteps;
}

View File

@ -10,6 +10,7 @@ import android.view.ViewGroup;
import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
@ -55,7 +56,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
private void setupChart() {
mChart.setBackgroundColor(BACKGROUND_COLOR);
mChart.setDescriptionColor(DESCRIPTION_COLOR);
mChart.getDescription().setTextColor(DESCRIPTION_COLOR);
configureBarLineChartDefaults(mChart);
@ -70,8 +71,8 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
y.setDrawGridLines(false);
// y.setDrawLabels(false);
// TODO: make fixed max value optional
y.setAxisMaxValue(1f);
y.setAxisMinValue(0);
y.setAxisMaximum(1f);
y.setAxisMinimum(0);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
@ -80,12 +81,12 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
YAxis yAxisRight = mChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(supportsHeartrate());
yAxisRight.setEnabled(supportsHeartrate(getChartsHost().getDevice()));
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);
yAxisRight.setAxisMaximum(HeartRateUtils.MAX_HEART_RATE_VALUE);
yAxisRight.setAxisMinimum(HeartRateUtils.MIN_HEART_RATE_VALUE);
// refresh immediately instead of use refreshIfVisible(), for perceived performance
refresh();
@ -108,7 +109,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
List<ActivitySample> samples = getSamples(db, device);
List<? extends ActivitySample> samples = getSamples(db, device);
return refresh(device, samples);
}
@ -116,33 +117,52 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
protected void updateChartsnUIThread(ChartsData chartsData) {
DefaultChartsData dcd = (DefaultChartsData) chartsData;
mChart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
mChart.setData(dcd.getCombinedData());
}
protected void renderCharts() {
mChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
}
protected void setupLegend(Chart chart) {
List<Integer> legendColors = new ArrayList<>(4);
List<String> legendLabels = new ArrayList<>(4);
legendColors.add(akActivity.color);
legendLabels.add(akActivity.label);
legendColors.add(akLightSleep.color);
legendLabels.add(akLightSleep.label);
legendColors.add(akDeepSleep.color);
legendLabels.add(akDeepSleep.label);
legendColors.add(akNotWorn.color);
legendLabels.add(akNotWorn.label);
if (supportsHeartrate()) {
legendColors.add(HEARTRATE_COLOR);
legendLabels.add(HEARTRATE_LABEL);
}
chart.getLegend().setCustom(legendColors, legendLabels);
mChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mChart.getXAxis().setValueFormatter(dcd.getXValueFormatter());
mChart.setData(dcd.getData());
}
@Override
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
protected void renderCharts() {
mChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
// mChart.invalidate();
}
@Override
protected void setupLegend(Chart chart) {
List<LegendEntry> legendEntries = new ArrayList<>(5);
LegendEntry activityEntry = new LegendEntry();
activityEntry.label = akActivity.label;
activityEntry.formColor = akActivity.color;
legendEntries.add(activityEntry);
LegendEntry lightSleepEntry = new LegendEntry();
lightSleepEntry.label = akLightSleep.label;
lightSleepEntry.formColor = akLightSleep.color;
legendEntries.add(lightSleepEntry);
LegendEntry deepSleepEntry = new LegendEntry();
deepSleepEntry.label = akDeepSleep.label;
deepSleepEntry.formColor = akDeepSleep.color;
legendEntries.add(deepSleepEntry);
LegendEntry notWornEntry = new LegendEntry();
notWornEntry.label = akNotWorn.label;
notWornEntry.formColor = akNotWorn.color;
legendEntries.add(notWornEntry);
if (supportsHeartrate(getChartsHost().getDevice())) {
LegendEntry hrEntry = new LegendEntry();
hrEntry.label = HEARTRATE_LABEL;
hrEntry.formColor = HEARTRATE_COLOR;
legendEntries.add(hrEntry);
}
chart.getLegend().setCustom(legendEntries);
}
@Override
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return getAllSamples(db, device, tsFrom, tsTo);
}
}

View File

@ -102,8 +102,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
swipeLayout.setRefreshing(true);
} else {
boolean wasBusy = swipeLayout.isRefreshing();
swipeLayout.setRefreshing(false);
if (wasBusy) {
swipeLayout.setRefreshing(false);
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
}
}

View File

@ -69,12 +69,13 @@ public class LiveActivityFragment extends AbstractChartFragment {
private List<Measurement> heartRateValues;
private LineDataSet mHeartRateSet;
private int mHeartRate;
private TimestampTranslation tsTranslation;
private class Steps {
private int initialSteps;
private int steps;
private long lastTimestamp;
private int lastTimestamp;
private int currentStepsPerMinute;
private int maxStepsPerMinute;
private int lastStepsPerMinute;
@ -96,7 +97,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
return maxStepsPerMinute;
}
public void updateCurrentSteps(int newSteps, long timestamp) {
public void updateCurrentSteps(int newSteps, int timestamp) {
try {
if (steps == 0) {
steps = newSteps;
@ -110,7 +111,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
if (newSteps >= steps) {
int stepsDelta = newSteps - steps;
long timeDelta = timestamp - lastTimestamp;
int timeDelta = timestamp - lastTimestamp;
currentStepsPerMinute = calculateStepsPerMinute(stepsDelta, timeDelta);
if (currentStepsPerMinute > maxStepsPerMinute) {
maxStepsPerMinute = currentStepsPerMinute;
@ -127,16 +128,16 @@ public class LiveActivityFragment extends AbstractChartFragment {
}
}
private int calculateStepsPerMinute(int stepsDelta, long millis) {
private int calculateStepsPerMinute(int stepsDelta, int seconds) {
if (stepsDelta == 0) {
return 0; // not walking or not enough data per mills?
}
if (millis <= 0) {
throw new IllegalArgumentException("delta in millis is <= 0 -- time change?");
if (seconds <= 0) {
throw new IllegalArgumentException("delta in seconds is <= 0 -- time change?");
}
long oneMinute = 60 * 1000;
float factor = oneMinute / millis;
int oneMinute = 60 * 1000;
float factor = oneMinute / seconds;
int result = (int) (stepsDelta * factor);
if (result > MAX_STEPS_PER_MINUTE) {
// ignore, return previous value instead
@ -153,13 +154,13 @@ public class LiveActivityFragment extends AbstractChartFragment {
switch (action) {
case DeviceService.ACTION_REALTIME_STEPS: {
int steps = intent.getIntExtra(DeviceService.EXTRA_REALTIME_STEPS, 0);
long timestamp = intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
int timestamp = translateTimestampFrom(intent);
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());
int timestamp = translateTimestampFrom(intent);
if (isValidHeartRateValue(heartRate)) {
setCurrentHeartRate(heartRate, timestamp);
}
@ -169,7 +170,16 @@ public class LiveActivityFragment extends AbstractChartFragment {
}
};
private void setCurrentHeartRate(int heartRate, long timestamp) {
private int translateTimestampFrom(Intent intent) {
return translateTimestamp(intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()));
}
private int translateTimestamp(long tsMillis) {
int timestamp = (int) (tsMillis / 1000); // translate to seconds
return tsTranslation.shorten(timestamp); // and shorten
}
private void setCurrentHeartRate(int heartRate, int timestamp) {
addHistoryDataSet(true);
mHeartRate = heartRate;
}
@ -180,7 +190,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
return result;
}
private void addEntries(int steps, long timestamp) {
private void addEntries(int steps, int timestamp) {
mSteps.updateCurrentSteps(steps, timestamp);
if (++maxStepsResetCounter > RESET_COUNT) {
maxStepsResetCounter = 0;
@ -192,7 +202,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
// addEntries();
}
private void addEntries() {
private void addEntries(int timestamp) {
mTotalStepsChart.setSingleEntryYValue(mSteps.getTotalSteps());
YAxis stepsPerMinuteCurrentYAxis = mStepsPerMinuteCurrentChart.getAxisLeft();
int maxStepsPerMinute = mSteps.getMaxStepsPerMinute();
@ -211,16 +221,15 @@ public class LiveActivityFragment extends AbstractChartFragment {
}
ChartData data = mStepsPerMinuteHistoryChart.getData();
data.addXValue("");
if (stepsPerMinute < 0) {
stepsPerMinute = 0;
}
mHistorySet.addEntry(new Entry(stepsPerMinute, data.getXValCount() - 1));
mHistorySet.addEntry(new Entry(timestamp, stepsPerMinute));
int hr = getCurrentHeartRate();
if (hr < 0) {
hr = 0;
}
mHeartRateSet.addEntry(new Entry(hr, data.getXValCount() - 1));
mHeartRateSet.addEntry(new Entry(timestamp, hr));
}
private boolean addHistoryDataSet(boolean force) {
@ -245,6 +254,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
filterLocal.addAction(DeviceService.ACTION_REALTIME_STEPS);
filterLocal.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT);
heartRateValues = new ArrayList<>();
tsTranslation = new TimestampTranslation();
View rootView = inflater.inflate(R.layout.fragment_live_activity, container, false);
@ -252,8 +262,8 @@ public class LiveActivityFragment extends AbstractChartFragment {
mTotalStepsChart = (CustomBarChart) rootView.findViewById(R.id.livechart_steps_total);
mStepsPerMinuteHistoryChart = (BarLineChartBase) rootView.findViewById(R.id.livechart_steps_per_minute_history);
totalStepsEntry = new BarEntry(0, 1);
stepsPerMinuteEntry = new BarEntry(0, 1);
totalStepsEntry = new BarEntry(1, 0);
stepsPerMinuteEntry = new BarEntry(1, 0);
mStepsPerMinuteData = setupCurrentChart(mStepsPerMinuteCurrentChart, stepsPerMinuteEntry, getString(R.string.live_activity_current_steps_per_minute));
mTotalStepsData = setupTotalStepsChart(mTotalStepsChart, totalStepsEntry, getString(R.string.live_activity_total_steps));
@ -305,7 +315,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
* Called in the UI thread.
*/
private void pulse() {
addEntries();
addEntries(translateTimestamp(System.currentTimeMillis()));
LineData historyData = (LineData) mStepsPerMinuteHistoryChart.getData();
if (historyData == null) {
@ -323,7 +333,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
}
private long getPulseIntervalMillis() {
private int getPulseIntervalMillis() {
return 1000;
}
@ -368,32 +378,31 @@ public class LiveActivityFragment extends AbstractChartFragment {
chart.getXAxis().setDrawLabels(false);
chart.getXAxis().setEnabled(false);
chart.setBackgroundColor(BACKGROUND_COLOR);
chart.setDescriptionColor(DESCRIPTION_COLOR);
chart.setDescription(title);
chart.setNoDataTextDescription("");
chart.getDescription().setTextColor(DESCRIPTION_COLOR);
chart.getDescription().setText(title);
// chart.setNoDataTextDescription("");
chart.setNoDataText("");
chart.getAxisRight().setEnabled(false);
List<BarEntry> entries = new ArrayList<>();
List<String> xLabels = new ArrayList<>();
List<Integer> colors = new ArrayList<>();
entries.add(new BarEntry(0, 0));
entries.add(entry);
entries.add(new BarEntry(0, 2));
entries.add(new BarEntry(2, 0));
colors.add(akActivity.color);
colors.add(akActivity.color);
colors.add(akActivity.color);
//we don't want labels
xLabels.add("");
xLabels.add("");
xLabels.add("");
// //we don't want labels
// xLabels.add("");
// xLabels.add("");
// xLabels.add("");
BarDataSet set = new BarDataSet(entries, "");
set.setDrawValues(false);
set.setColors(colors);
BarData data = new BarData(xLabels, set);
data.setGroupSpace(0);
BarData data = new BarData(set);
// data.setGroupSpace(0);
chart.setData(data);
chart.getLegend().setEnabled(false);
@ -402,7 +411,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
}
private BarDataSet setupTotalStepsChart(CustomBarChart chart, BarEntry entry, String label) {
mTotalStepsChart.getAxisLeft().setAxisMaxValue(5000); // TODO: use daily goal - already reached steps
mTotalStepsChart.getAxisLeft().setAxisMaximum(5000); // TODO: use daily goal - already reached steps
return setupCommonChart(chart, entry, label); // at the moment, these look the same
}
@ -411,8 +420,8 @@ public class LiveActivityFragment extends AbstractChartFragment {
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));
chart.getDescription().setTextColor(DESCRIPTION_COLOR);
chart.getDescription().setText(getString(R.string.live_activity_steps_per_minute_history));
chart.setNoDataText(getString(R.string.live_activity_start_your_activity));
chart.getLegend().setEnabled(false);
Paint infoPaint = chart.getPaint(Chart.PAINT_INFO);
@ -432,7 +441,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
y.setEnabled(true);
y.setAxisMinValue(0);
y.setAxisMinimum(0);
YAxis yAxisRight = chart.getAxisRight();
yAxisRight.setDrawGridLines(false);
@ -440,14 +449,14 @@ public class LiveActivityFragment extends AbstractChartFragment {
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);
yAxisRight.setAxisMaximum(HeartRateUtils.MAX_HEART_RATE_VALUE);
yAxisRight.setAxisMinimum(HeartRateUtils.MIN_HEART_RATE_VALUE);
mHistorySet = new LineDataSet(new ArrayList<Entry>(), getString(R.string.live_activity_steps_history));
mHistorySet.setAxisDependency(YAxis.AxisDependency.LEFT);
mHistorySet.setColor(akActivity.color);
mHistorySet.setDrawCircles(false);
mHistorySet.setDrawCubic(true);
mHistorySet.setMode(LineDataSet.Mode.CUBIC_BEZIER);
mHistorySet.setDrawFilled(true);
mHistorySet.setDrawValues(false);

View File

@ -23,8 +23,8 @@ public class SingleEntryValueAnimator extends ChartAnimator {
}
public void setEntryYValue(float value) {
this.previousValue = entry.getVal();
entry.setVal(value);
this.previousValue = entry.getY();
entry.setY(value);
}
@Override
@ -38,10 +38,10 @@ public class SingleEntryValueAnimator extends ChartAnimator {
float startAnim;
float endAnim = 1f;
if (entry.getVal() == 0f) {
if (entry.getY() == 0f) {
startAnim = 0f;
} else {
startAnim = previousValue / entry.getVal();
startAnim = previousValue / entry.getY();
}
// LOG.debug("anim factors: " + startAnim + ", " + endAnim);

View File

@ -11,12 +11,15 @@ import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.charts.CombinedChart;
import com.github.mikephil.charting.charts.PieChart;
import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.CombinedData;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.formatter.ValueFormatter;
import com.github.mikephil.charting.data.PieEntry;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.utils.ViewPortHandler;
import org.slf4j.Logger;
@ -50,7 +53,7 @@ public class SleepChartFragment extends AbstractChartFragment {
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
List<ActivitySample> samples = getSamples(db, device);
List<? extends ActivitySample> samples = getSamples(db, device);
MySleepChartsData mySleepChartsData = refreshSleepAmounts(device, samples);
DefaultChartsData chartsData = refresh(device, samples);
@ -58,26 +61,27 @@ public class SleepChartFragment extends AbstractChartFragment {
return new MyChartsData(mySleepChartsData, chartsData);
}
private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List<ActivitySample> samples) {
private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List<? extends ActivitySample> samples) {
ActivityAnalysis analysis = new ActivityAnalysis();
ActivityAmounts amounts = analysis.calculateActivityAmounts(samples);
PieData data = new PieData();
List<Entry> entries = new ArrayList<>();
List<PieEntry> entries = new ArrayList<>();
List<Integer> colors = new ArrayList<>();
int index = 0;
// int index = 0;
long totalSeconds = 0;
for (ActivityAmount amount : amounts.getAmounts()) {
if ((amount.getActivityKind() & ActivityKind.TYPE_SLEEP) != 0) {
long value = amount.getTotalSeconds();
totalSeconds += value;
entries.add(new Entry(value, index++));
// entries.add(new PieEntry(value, index++));
entries.add(new PieEntry(value, amount.getName(getActivity())));
colors.add(getColorFor(amount.getActivityKind()));
data.addXValue(amount.getName(getActivity()));
// data.addXValue(amount.getName(getActivity()));
}
}
String totalSleep = DateTimeUtils.formatDurationHoursMinutes(totalSeconds, TimeUnit.SECONDS);
PieDataSet set = new PieDataSet(entries, "");
set.setValueFormatter(new ValueFormatter() {
set.setValueFormatter(new IValueFormatter() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS);
@ -96,7 +100,9 @@ public class SleepChartFragment extends AbstractChartFragment {
mSleepAmountChart.setCenterText(mcd.getPieData().getTotalSleep());
mSleepAmountChart.setData(mcd.getPieData().getPieData());
mActivityChart.setData(mcd.getChartsData().getCombinedData());
mActivityChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mActivityChart.getXAxis().setValueFormatter(mcd.getChartsData().getXValueFormatter());
mActivityChart.setData(mcd.getChartsData().getData());
}
@Override
@ -138,16 +144,16 @@ public class SleepChartFragment extends AbstractChartFragment {
private void setupSleepAmountChart() {
mSleepAmountChart.setBackgroundColor(BACKGROUND_COLOR);
mSleepAmountChart.setDescriptionColor(DESCRIPTION_COLOR);
mSleepAmountChart.setDescription("");
mSleepAmountChart.setNoDataTextDescription("");
mSleepAmountChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mSleepAmountChart.getDescription().setText("");
// mSleepAmountChart.getDescription().setNoDataTextDescription("");
mSleepAmountChart.setNoDataText("");
mSleepAmountChart.getLegend().setEnabled(false);
}
private void setupActivityChart() {
mActivityChart.setBackgroundColor(BACKGROUND_COLOR);
mActivityChart.setDescriptionColor(DESCRIPTION_COLOR);
mActivityChart.getDescription().setTextColor(DESCRIPTION_COLOR);
configureBarLineChartDefaults(mActivityChart);
XAxis x = mActivityChart.getXAxis();
@ -161,8 +167,8 @@ public class SleepChartFragment extends AbstractChartFragment {
y.setDrawGridLines(false);
// y.setDrawLabels(false);
// TODO: make fixed max value optional
y.setAxisMaxValue(1f);
y.setAxisMinValue(0);
y.setAxisMaximum(1f);
y.setAxisMinimum(0);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
@ -171,7 +177,7 @@ public class SleepChartFragment extends AbstractChartFragment {
YAxis yAxisRight = mActivityChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(supportsHeartrate());
yAxisRight.setEnabled(supportsHeartrate(getChartsHost().getDevice()));
yAxisRight.setDrawLabels(true);
yAxisRight.setDrawTopYLabelEntry(true);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
@ -179,28 +185,37 @@ public class SleepChartFragment extends AbstractChartFragment {
yAxisRight.setAxisMinValue(HeartRateUtils.MIN_HEART_RATE_VALUE);
}
@Override
protected void setupLegend(Chart chart) {
List<Integer> legendColors = new ArrayList<>(2);
List<String> legendLabels = new ArrayList<>(2);
legendColors.add(akLightSleep.color);
legendLabels.add(akLightSleep.label);
legendColors.add(akDeepSleep.color);
legendLabels.add(akDeepSleep.label);
if (supportsHeartrate()) {
legendColors.add(HEARTRATE_COLOR);
legendLabels.add(HEARTRATE_LABEL);
List<LegendEntry> legendEntries = new ArrayList<>(3);
LegendEntry lightSleepEntry = new LegendEntry();
lightSleepEntry.label = akLightSleep.label;
lightSleepEntry.formColor = akLightSleep.color;
legendEntries.add(lightSleepEntry);
LegendEntry deepSleepEntry = new LegendEntry();
deepSleepEntry.label = akDeepSleep.label;
deepSleepEntry.formColor = akDeepSleep.color;
legendEntries.add(deepSleepEntry);
if (supportsHeartrate(getChartsHost().getDevice())) {
LegendEntry hrEntry = new LegendEntry();
hrEntry.label = HEARTRATE_LABEL;
hrEntry.formColor = HEARTRATE_COLOR;
legendEntries.add(hrEntry);
}
chart.getLegend().setCustom(legendColors, legendLabels);
chart.getLegend().setCustom(legendEntries);
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
@Override
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
// temporary fix for totally wrong sleep amounts
// return super.getSleepSamples(db, device, tsFrom, tsTo);
return super.getAllSamples(db, device, tsFrom, tsTo);
}
@Override
protected void renderCharts() {
mActivityChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
mSleepAmountChart.invalidate();
@ -225,10 +240,10 @@ public class SleepChartFragment extends AbstractChartFragment {
}
private static class MyChartsData extends ChartsData {
private final DefaultChartsData chartsData;
private final DefaultChartsData<CombinedData> chartsData;
private final MySleepChartsData pieData;
public MyChartsData(MySleepChartsData pieData, DefaultChartsData chartsData) {
public MyChartsData(MySleepChartsData pieData, DefaultChartsData<CombinedData> chartsData) {
this.pieData = pieData;
this.chartsData = chartsData;
}
@ -237,7 +252,7 @@ public class SleepChartFragment extends AbstractChartFragment {
return pieData;
}
public DefaultChartsData getChartsData() {
public DefaultChartsData<CombinedData> getChartsData() {
return chartsData;
}
}

View File

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

View File

@ -0,0 +1,39 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
public class TrailingActivitySample extends AbstractActivitySample {
private int timestamp;
private long userId;
private long deviceId;
@Override
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
@Override
public void setUserId(long userId) {
this.userId = userId;
}
@Override
public void setDeviceId(long deviceId) {
this.deviceId = deviceId;
}
@Override
public long getDeviceId() {
return deviceId;
}
@Override
public long getUserId() {
return userId;
}
@Override
public int getTimestamp() {
return timestamp;
}
}

View File

@ -6,8 +6,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.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,10 +15,9 @@ 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;
import com.github.mikephil.charting.data.PieEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -43,7 +42,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
private int mTargetSteps = 10000;
private PieChart mTodayStepsChart;
private CombinedChart mWeekStepsChart;
private BarChart mWeekStepsChart;
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
@ -64,8 +63,10 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
mTodayStepsChart.setCenterText(NumberFormat.getNumberInstance(mLocale).format(mcd.getDaySteps().totalSteps));
mTodayStepsChart.setData(mcd.getDaySteps().data);
mWeekStepsChart.setData(mcd.getWeekBeforeStepsData().getCombinedData());
mWeekStepsChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mWeekStepsChart.setData(mcd.getWeekBeforeStepsData().getData());
mWeekStepsChart.getLegend().setEnabled(false);
mWeekStepsChart.getXAxis().setValueFormatter(mcd.getWeekBeforeStepsData().getXValueFormatter());
}
@Override
@ -74,17 +75,17 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
mTodayStepsChart.invalidate();
}
private DefaultChartsData refreshWeekBeforeSteps(DBHandler db, CombinedChart combinedChart, Calendar day, GBDevice device) {
private DefaultChartsData<BarData> refreshWeekBeforeSteps(DBHandler db, BarChart barChart, Calendar day, GBDevice device) {
ActivityAnalysis analysis = new ActivityAnalysis();
day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -7);
List<BarEntry> entries = new ArrayList<>();
List<String> labels = new ArrayList<>();
ArrayList<String> labels = new ArrayList<String>();
for (int counter = 0; counter < 7; counter++) {
entries.add(new BarEntry(analysis.calculateTotalSteps(getSamplesOfDay(db, day, device)), counter));
entries.add(new BarEntry(counter, analysis.calculateTotalSteps(getSamplesOfDay(db, day, device))));
labels.add(day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
day.add(Calendar.DATE, 1);
}
@ -92,39 +93,32 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
BarDataSet set = new BarDataSet(entries, "");
set.setColor(akActivity.color);
BarData barData = new BarData(labels, set);
BarData barData = new BarData(set);
barData.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false);
LimitLine target = new LimitLine(mTargetSteps);
combinedChart.getAxisLeft().removeAllLimitLines();
combinedChart.getAxisLeft().addLimitLine(target);
barChart.getAxisLeft().removeAllLimitLines();
barChart.getAxisLeft().addLimitLine(target);
CombinedData combinedData = new CombinedData(labels);
combinedData.setData(barData);
return new DefaultChartsData(combinedData);
return new DefaultChartsData(barData, new PreformattedXIndexLabelFormatter(labels));
}
private DaySteps refreshDaySteps(DBHandler db, Calendar day, GBDevice device) {
ActivityAnalysis analysis = new ActivityAnalysis();
int totalSteps = analysis.calculateTotalSteps(getSamplesOfDay(db, day, device));
PieData data = new PieData();
List<Entry> entries = new ArrayList<>();
List<PieEntry> entries = new ArrayList<>();
List<Integer> colors = new ArrayList<>();
entries.add(new Entry(totalSteps, 0));
entries.add(new PieEntry(totalSteps, "")); //we don't want labels on the pie chart
colors.add(akActivity.color);
//we don't want labels on the pie chart
data.addXValue("");
if (totalSteps < mTargetSteps) {
entries.add(new Entry((mTargetSteps - totalSteps), 1));
entries.add(new PieEntry((mTargetSteps - totalSteps))); //we don't want labels on the pie chart
colors.add(Color.GRAY);
//we don't want labels on the pie chart
data.addXValue("");
}
PieDataSet set = new PieDataSet(entries, "");
@ -141,7 +135,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
Bundle savedInstanceState) {
mLocale = getResources().getConfiguration().locale;
View rootView = inflater.inflate(R.layout.fragment_sleepchart, container, false);
View rootView = inflater.inflate(R.layout.fragment_weeksteps_chart, container, false);
GBDevice device = getChartsHost().getDevice();
if (device != null) {
@ -149,8 +143,8 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
mTargetSteps = MiBandCoordinator.getFitnessGoal(device.getAddress());
}
mWeekStepsChart = (CombinedChart) rootView.findViewById(R.id.sleepchart);
mTodayStepsChart = (PieChart) rootView.findViewById(R.id.sleepchart_pie_light_deep);
mTodayStepsChart = (PieChart) rootView.findViewById(R.id.todaystepschart);
mWeekStepsChart = (BarChart) rootView.findViewById(R.id.weekstepschart);
setupWeekStepsChart();
setupTodayStepsChart();
@ -168,9 +162,9 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
private void setupTodayStepsChart() {
mTodayStepsChart.setBackgroundColor(BACKGROUND_COLOR);
mTodayStepsChart.setDescriptionColor(DESCRIPTION_COLOR);
mTodayStepsChart.setDescription(getContext().getString(R.string.weeksteps_today_steps_description, mTargetSteps));
mTodayStepsChart.setNoDataTextDescription("");
mTodayStepsChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mTodayStepsChart.getDescription().setText(getContext().getString(R.string.weeksteps_today_steps_description, String.valueOf(mTargetSteps)));
// mTodayStepsChart.setNoDataTextDescription("");
mTodayStepsChart.setNoDataText("");
mTodayStepsChart.getLegend().setEnabled(false);
// setupLegend(mTodayStepsChart);
@ -178,8 +172,9 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
private void setupWeekStepsChart() {
mWeekStepsChart.setBackgroundColor(BACKGROUND_COLOR);
mWeekStepsChart.setDescriptionColor(DESCRIPTION_COLOR);
mWeekStepsChart.setDescription("");
mWeekStepsChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mWeekStepsChart.getDescription().setText("");
mWeekStepsChart.setFitBars(true);
configureBarLineChartDefaults(mWeekStepsChart);
@ -189,11 +184,15 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
x.setEnabled(true);
x.setTextColor(CHART_TEXT_COLOR);
x.setDrawLimitLinesBehindData(true);
x.setPosition(XAxis.XAxisPosition.BOTTOM);
YAxis y = mWeekStepsChart.getAxisLeft();
y.setDrawGridLines(false);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
y.setDrawZeroLine(true);
y.setSpaceBottom(0);
y.setAxisMinimum(0);
y.setEnabled(true);
@ -205,6 +204,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
yAxisRight.setTextColor(CHART_TEXT_COLOR);
}
@Override
protected void setupLegend(Chart chart) {
// List<Integer> legendColors = new ArrayList<>(1);
// List<String> legendLabels = new ArrayList<>(1);
@ -214,7 +214,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
// chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
private List<ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) {
private List<? extends ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) {
int startTs;
int endTs;
@ -233,7 +233,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
}
@Override
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return super.getAllSamples(db, device, tsFrom, tsTo);
}
@ -248,10 +248,10 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
}
private static class MyChartsData extends ChartsData {
private final DefaultChartsData weekBeforeStepsData;
private final DefaultChartsData<BarData> weekBeforeStepsData;
private final DaySteps daySteps;
public MyChartsData(DaySteps daySteps, DefaultChartsData weekBeforeStepsData) {
public MyChartsData(DaySteps daySteps, DefaultChartsData<BarData> weekBeforeStepsData) {
this.daySteps = daySteps;
this.weekBeforeStepsData = weekBeforeStepsData;
}
@ -260,7 +260,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
return daySteps;
}
public DefaultChartsData getWeekBeforeStepsData() {
public DefaultChartsData<BarData> getWeekBeforeStepsData() {
return weekBeforeStepsData;
}
}

View File

@ -51,6 +51,7 @@ public class DeviceCandidateAdapter extends ArrayAdapter<GBDeviceCandidate> {
deviceImageView.setImageResource(R.drawable.ic_device_pebble);
break;
case MIBAND:
case MIBAND2:
deviceImageView.setImageResource(R.drawable.ic_device_miband);
break;
default:

View File

@ -120,12 +120,20 @@ public class GBDeviceAdapter extends ArrayAdapter<GBDevice> {
}
break;
case MIBAND:
case MIBAND2:
if (device.isConnected()) {
deviceImageView.setImageResource(R.drawable.ic_device_miband);
} else {
deviceImageView.setImageResource(R.drawable.ic_device_miband_disabled);
}
break;
case VIBRATISSIMO:
if (device.isConnected()) {
deviceImageView.setImageResource(R.drawable.ic_device_lovetoy);
} else {
deviceImageView.setImageResource(R.drawable.ic_device_lovetoy_disabled);
}
break;
default:
if (device.isConnected()) {
deviceImageView.setImageResource(R.drawable.ic_launcher);
@ -160,8 +168,8 @@ public class GBDeviceAdapter extends ArrayAdapter<GBDevice> {
private String getUniqueDeviceName(GBDevice device) {
String deviceName = device.getName();
if (!isUniqueDeviceName(device, deviceName)) {
if (device.getHardwareVersion() != null) {
deviceName = deviceName + " " + device.getHardwareVersion();
if (device.getModel() != null) {
deviceName = deviceName + " " + device.getModel();
if (!isUniqueDeviceName(device, deviceName)) {
deviceName = deviceName + " " + device.getShortAddress();
}

View File

@ -4,69 +4,102 @@ import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
import com.woxthebox.draglistview.DragItemAdapter;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
/**
* Adapter for displaying GBDeviceApp instances.
*/
public class GBDeviceAppAdapter extends ArrayAdapter<GBDeviceApp> {
private final Context context;
public class GBDeviceAppAdapter extends DragItemAdapter<GBDeviceApp, GBDeviceAppAdapter.ViewHolder> {
public GBDeviceAppAdapter(Context context, List<GBDeviceApp> appList) {
super(context, 0, appList);
private final int mLayoutId;
private final int mGrabHandleId;
private final Context mContext;
private final AbstractAppManagerFragment mParentFragment;
this.context = context;
public GBDeviceAppAdapter(List<GBDeviceApp> list, int layoutId, int grabHandleId, Context context, AbstractAppManagerFragment parentFragment) {
super(true); // longpress
mLayoutId = layoutId;
mGrabHandleId = grabHandleId;
mContext = context;
mParentFragment = parentFragment;
setHasStableIds(true);
setItemList(list);
}
@Override
public View getView(int position, View view, ViewGroup parent) {
GBDeviceApp deviceApp = getItem(position);
public long getItemId(int position) {
return mItemList.get(position).getUUID().getLeastSignificantBits();
}
if (view == null) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
view = inflater.inflate(R.layout.item_with_details, parent, false);
}
TextView deviceAppVersionAuthorLabel = (TextView) view.findViewById(R.id.item_details);
TextView deviceAppNameLabel = (TextView) view.findViewById(R.id.item_name);
ImageView deviceImageView = (ImageView) view.findViewById(R.id.item_image);
View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
return new ViewHolder(view);
}
deviceAppVersionAuthorLabel.setText(getContext().getString(R.string.appversion_by_creator, deviceApp.getVersion(), deviceApp.getCreator()));
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
GBDeviceApp deviceApp = mItemList.get(position);
holder.mDeviceAppVersionAuthorLabel.setText(GBApplication.getContext().getString(R.string.appversion_by_creator, deviceApp.getVersion(), deviceApp.getCreator()));
// FIXME: replace with small icons
String appNameLabelText = deviceApp.getName();
if (deviceApp.isInCache() || deviceApp.isOnDevice()) {
appNameLabelText += " (" + (deviceApp.isInCache() ? "C" : "")
+ (deviceApp.isOnDevice() ? "D" : "") + ")";
}
deviceAppNameLabel.setText(appNameLabelText);
holder.mDeviceAppNameLabel.setText(appNameLabelText);
switch (deviceApp.getType()) {
case APP_GENERIC:
deviceImageView.setImageResource(R.drawable.ic_watchapp);
holder.mDeviceImageView.setImageResource(R.drawable.ic_watchapp);
break;
case APP_ACTIVITYTRACKER:
deviceImageView.setImageResource(R.drawable.ic_activitytracker);
holder.mDeviceImageView.setImageResource(R.drawable.ic_activitytracker);
break;
case APP_SYSTEM:
deviceImageView.setImageResource(R.drawable.ic_systemapp);
holder.mDeviceImageView.setImageResource(R.drawable.ic_systemapp);
break;
case WATCHFACE:
deviceImageView.setImageResource(R.drawable.ic_watchface);
holder.mDeviceImageView.setImageResource(R.drawable.ic_watchface);
break;
default:
deviceImageView.setImageResource(R.drawable.ic_watchapp);
holder.mDeviceImageView.setImageResource(R.drawable.ic_watchapp);
}
}
public class ViewHolder extends DragItemAdapter<GBDeviceApp, GBDeviceAppAdapter.ViewHolder>.ViewHolder {
TextView mDeviceAppVersionAuthorLabel;
TextView mDeviceAppNameLabel;
ImageView mDeviceImageView;
public ViewHolder(final View itemView) {
super(itemView, mGrabHandleId);
mDeviceAppVersionAuthorLabel = (TextView) itemView.findViewById(R.id.item_details);
mDeviceAppNameLabel = (TextView) itemView.findViewById(R.id.item_name);
mDeviceImageView = (ImageView) itemView.findViewById(R.id.item_image);
}
return view;
@Override
public void onItemClicked(View view) {
UUID uuid = mItemList.get(getAdapterPosition()).getUUID();
GBApplication.deviceService().onAppStart(uuid, true);
}
@Override
public boolean onItemLongClicked(View view) {
return mParentFragment.openPopupMenu(view, getAdapterPosition());
}
}
}

View File

@ -1,43 +1,35 @@
package nodomain.freeyourgadget.gadgetbridge.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.schema.ActivityDBCreationScript;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.impl.GBActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.database.schema.SchemaMigration;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
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;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
/**
* @deprecated can be removed entirely, only used for backwards compatibility
*/
public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandler {
private static final Logger LOG = LoggerFactory.getLogger(ActivityDatabaseHandler.class);
private static final int DATABASE_VERSION = 7;
private static final String UPDATER_CLASS_NAME_PREFIX = "ActivityDBUpdate_";
private final Context context;
public ActivityDatabaseHandler(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
this.context = context;
}
@Override
@ -52,135 +44,25 @@ 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);
if (updater != null) {
LOG.info("upgrading activity database to version " + i);
updater.upgradeSchema(db);
}
}
LOG.info("activity database is now at version " + newVersion);
} catch (RuntimeException ex) {
GB.toast("Error upgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
throw ex; // reject upgrade
}
new SchemaMigration(UPDATER_CLASS_NAME_PREFIX).onUpgrade(db, oldVersion, newVersion);
}
@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);
if (updater != null) {
LOG.info("downgrading activity database to version " + (i - 1));
updater.downgradeSchema(db);
}
}
LOG.info("activity database is now at version " + newVersion);
} catch (RuntimeException ex) {
GB.toast("Error downgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
throw ex; // reject downgrade
}
}
private DBUpdateScript getUpdateScript(SQLiteDatabase db, int version) {
try {
Class<?> updateClass = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + ".schema.ActivityDBUpdate_" + version);
return (DBUpdateScript) updateClass.newInstance();
} catch (ClassNotFoundException e) {
return null;
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Error instantiating DBUpdate class for version " + version, e);
}
}
public void addGBActivitySample(ActivitySample sample) {
try (SQLiteDatabase db = this.getWritableDatabase()) {
ContentValues values = new ContentValues();
values.put(KEY_TIMESTAMP, sample.getTimestamp());
values.put(KEY_PROVIDER, sample.getProvider().getID());
values.put(KEY_INTENSITY, sample.getRawIntensity());
values.put(KEY_STEPS, sample.getSteps());
values.put(KEY_CUSTOM_SHORT, sample.getCustomValue());
values.put(KEY_TYPE, sample.getRawKind());
db.insert(TABLE_GBACTIVITYSAMPLES, null, values);
}
}
/**
* Adds the a new sample to the database
*
* @param timestamp the timestamp of the same, second-based!
* @param provider the SampleProvider ID
* @param intensity the sample's raw intensity value
* @param steps the sample's steps value
* @param kind the raw activity kind of the sample
* @param customShortValue
*/
@Override
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;
}
if (steps < 0) {
LOG.error("negative steps received, ignoring");
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);
values.put(KEY_PROVIDER, provider);
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);
}
new SchemaMigration(UPDATER_CLASS_NAME_PREFIX).onDowngrade(db, oldVersion, newVersion);
}
@Override
public void addGBActivitySamples(ActivitySample[] activitySamples) {
try (SQLiteDatabase db = this.getWritableDatabase()) {
String sql = "INSERT INTO " + TABLE_GBACTIVITYSAMPLES + " (" + KEY_TIMESTAMP + "," +
KEY_PROVIDER + "," + KEY_INTENSITY + "," + KEY_STEPS + "," + KEY_TYPE + "," + KEY_CUSTOM_SHORT + ")" +
" VALUES (?,?,?,?,?,?);";
SQLiteStatement statement = db.compileStatement(sql);
db.beginTransaction();
for (ActivitySample activitySample : activitySamples) {
statement.clearBindings();
statement.bindLong(1, activitySample.getTimestamp());
statement.bindLong(2, activitySample.getProvider().getID());
statement.bindLong(3, activitySample.getRawIntensity());
statement.bindLong(4, activitySample.getSteps());
statement.bindLong(5, activitySample.getRawKind());
statement.bindLong(6, activitySample.getCustomValue());
statement.execute();
}
db.setTransactionSuccessful();
db.endTransaction();
}
public SQLiteDatabase getDatabase() {
return super.getWritableDatabase();
}
public ArrayList<ActivitySample> getSleepSamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_SLEEP, provider);
@Override
public void closeDb() {
}
public ArrayList<ActivitySample> getActivitySamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ACTIVITY, provider);
@Override
public void openDb() {
}
@Override
@ -188,120 +70,36 @@ public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandl
return this;
}
@Override
public void release() {
GBApplication.releaseDB();
public Context getContext() {
return context;
}
public ArrayList<ActivitySample> getAllActivitySamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL, provider);
}
public boolean hasContent() {
File dbFile = getContext().getDatabasePath(getDatabaseName());
if (dbFile == null || !dbFile.exists()) {
return false;
}
/**
* Returns all available activity samples from between the two timestamps (inclusive), of the given
* provided and type(s).
*
* @param timestamp_from
* @param timestamp_to
* @param activityTypes ORed combination of #TYPE_DEEP_SLEEP, #TYPE_LIGHT_SLEEP, #TYPE_ACTIVITY
* @param provider the producer of the samples to be sought
* @return
*/
private ArrayList<ActivitySample> getGBActivitySamples(int timestamp_from, int timestamp_to, int activityTypes, SampleProvider provider) {
if (timestamp_to < 0) {
throw new IllegalArgumentException("negative timestamp_to");
}
if (timestamp_from < 0) {
throw new IllegalArgumentException("negative timestamp_from");
}
ArrayList<ActivitySample> samples = new ArrayList<>();
final String where = "(provider=" + provider.getID() + " and timestamp>=" + timestamp_from + " and timestamp<=" + timestamp_to + getWhereClauseFor(activityTypes, provider) + ")";
LOG.info("Activity query where: " + where);
final String order = "timestamp";
try (SQLiteDatabase db = this.getReadableDatabase()) {
try (Cursor cursor = db.query(TABLE_GBACTIVITYSAMPLES, null, where, null, null, null, order)) {
LOG.info("Activity query result: " + cursor.getCount() + " samples");
int colTimeStamp = cursor.getColumnIndex(KEY_TIMESTAMP);
int colIntensity = cursor.getColumnIndex(KEY_INTENSITY);
int colSteps = cursor.getColumnIndex(KEY_STEPS);
int colType = cursor.getColumnIndex(KEY_TYPE);
int colCustomShort = cursor.getColumnIndex(KEY_CUSTOM_SHORT);
while (cursor.moveToNext()) {
GBActivitySample sample = new GBActivitySample(
provider,
cursor.getInt(colTimeStamp),
cursor.getInt(colIntensity),
cursor.getInt(colSteps),
cursor.getInt(colType),
cursor.getInt(colCustomShort));
samples.add(sample);
try {
try (SQLiteDatabase db = this.getReadableDatabase()) {
try (Cursor cursor = db.query(TABLE_GBACTIVITYSAMPLES, new String[]{KEY_TIMESTAMP}, null, null, null, null, null, "1")) {
return cursor.moveToFirst();
}
}
}
return samples;
}
private String getWhereClauseFor(int activityTypes, SampleProvider provider) {
if (activityTypes == ActivityKind.TYPE_ALL) {
return ""; // no further restriction
}
StringBuilder builder = new StringBuilder(" and (");
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) {
builder.append(" or ");
}
}
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();
} catch (Exception ex) {
// can't expect anything
GB.log("Error looking for old activity data: " + ex.getMessage(), GB.ERROR, ex);
return false;
}
}
@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();
}
public DaoSession getDaoSession() {
throw new UnsupportedOperationException();
}
@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;
public DaoMaster getDaoMaster() {
throw new UnsupportedOperationException();
}
}

View File

@ -26,16 +26,10 @@ public abstract class DBAccess extends AsyncTask {
@Override
protected Object doInBackground(Object[] params) {
DBHandler handler = null;
try {
handler = GBApplication.acquireDB();
doInBackground(handler);
try (DBHandler db = GBApplication.acquireDB()) {
doInBackground(db);
} catch (Exception e) {
mError = e;
} finally {
if (handler != null) {
handler.release();
}
}
return null;
}

View File

@ -1,10 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.database;
/**
* TODO: Legacy, can be removed once migration support for old ActivityDatabase is removed
* @deprecated only for backwards compatibility
*/
public class DBConstants {
public static final String DATABASE_NAME = "ActivityDatabase";
public static final String TABLE_GBACTIVITYSAMPLES = "GBActivitySamples";
public static final String TABLE_STEPS_PER_DAY = "StepsPerDay";
public static final String KEY_TIMESTAMP = "timestamp";
public static final String KEY_PROVIDER = "provider";

View File

@ -3,37 +3,34 @@ package nodomain.freeyourgadget.gadgetbridge.database;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
/**
* Provides lowlevel access to the database.
*/
public interface DBHandler extends AutoCloseable {
/**
* Closes the database.
*/
void closeDb();
/**
* Opens the database. Note that this is only possible after an explicit
* #closeDb(). Initially the db is implicitly open.
*/
void openDb();
public interface DBHandler {
SQLiteOpenHelper getHelper();
/**
* Releases the DB handler. No access may be performed after calling this method.
* Same as calling {@link GBApplication#releaseDB()}
* Releases the DB handler. No DB access will be possible before
* #openDb() will be called.
*/
void release();
void close() throws Exception;
List<ActivitySample> getAllActivitySamples(int tsFrom, int tsTo, SampleProvider provider);
List<ActivitySample> getActivitySamples(int tsFrom, int tsTo, SampleProvider provider);
List<ActivitySample> getSleepSamples(int tsFrom, int tsTo, SampleProvider provider);
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);
SQLiteDatabase getDatabase();
DaoMaster getDaoMaster();
DaoSession getDaoSession();
}

View File

@ -4,58 +4,132 @@ import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.Query;
import de.greenrobot.dao.query.QueryBuilder;
import de.greenrobot.dao.query.WhereCondition;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleHealthSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleMisfitSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.ActivityDescription;
import nodomain.freeyourgadget.gadgetbridge.entities.ActivityDescriptionDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributes;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlay;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlayDao;
import nodomain.freeyourgadget.gadgetbridge.entities.Tag;
import nodomain.freeyourgadget.gadgetbridge.entities.TagDao;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.entities.UserAttributes;
import nodomain.freeyourgadget.gadgetbridge.entities.UserDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.ValidByDate;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
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_STEPS;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
/**
* Provides utiliy access to some common entities, so you won't need to use
* their DAO classes.
* <p/>
* Maybe this code should actually be in the DAO classes themselves, but then
* these should be under revision control instead of 100% generated at build time.
*/
public class DBHelper {
private static final Logger LOG = LoggerFactory.getLogger(DBHelper.class);
private final Context context;
public DBHelper(Context context) {
this.context = context;
}
private String getClosedDBPath(SQLiteOpenHelper dbHandler) throws IllegalStateException {
SQLiteDatabase db = dbHandler.getReadableDatabase();
/**
* Closes the database and returns its name.
* Important: after calling this, you have to DBHandler#openDb() it again
* to get it back to work.
*
* @param dbHandler
* @return
* @throws IllegalStateException
*/
private String getClosedDBPath(DBHandler dbHandler) throws IllegalStateException {
SQLiteDatabase db = dbHandler.getDatabase();
String path = db.getPath();
db.close();
dbHandler.closeDb();
if (db.isOpen()) { // reference counted, so may still be open
throw new IllegalStateException("Database must be closed");
}
return path;
}
public File exportDB(SQLiteOpenHelper dbHandler, File toDir) throws IllegalStateException, IOException {
public File exportDB(DBHandler dbHandler, File toDir) throws IllegalStateException, IOException {
String dbPath = getClosedDBPath(dbHandler);
File sourceFile = new File(dbPath);
File destFile = new File(toDir, sourceFile.getName());
if (destFile.exists()) {
File backup = new File(toDir, destFile.getName() + "_" + getDate());
destFile.renameTo(backup);
} else if (!toDir.exists()) {
if (!toDir.mkdirs()) {
throw new IOException("Unable to create directory: " + toDir.getAbsolutePath());
try {
File sourceFile = new File(dbPath);
File destFile = new File(toDir, sourceFile.getName());
if (destFile.exists()) {
File backup = new File(toDir, destFile.getName() + "_" + getDate());
destFile.renameTo(backup);
} else if (!toDir.exists()) {
if (!toDir.mkdirs()) {
throw new IOException("Unable to create directory: " + toDir.getAbsolutePath());
}
}
}
FileUtils.copyFile(sourceFile, destFile);
return destFile;
FileUtils.copyFile(sourceFile, destFile);
return destFile;
} finally {
dbHandler.openDb();
}
}
private String getDate() {
return new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(new Date());
}
public void importDB(SQLiteOpenHelper dbHandler, File fromFile) throws IllegalStateException, IOException {
public void importDB(DBHandler dbHandler, File fromFile) throws IllegalStateException, IOException {
String dbPath = getClosedDBPath(dbHandler);
File toFile = new File(dbPath);
FileUtils.copyFile(fromFile, toFile);
try {
File toFile = new File(dbPath);
FileUtils.copyFile(fromFile, toFile);
} finally {
dbHandler.openDb();
}
}
public void validateDB(SQLiteOpenHelper dbHandler) throws IOException {
@ -71,6 +145,11 @@ public class DBHelper {
db.execSQL(statement);
}
public boolean existsDB(String dbName) {
File path = context.getDatabasePath(dbName);
return path != null && path.exists();
}
public static boolean existsColumn(String tableName, String columnName, SQLiteDatabase db) {
try (Cursor res = db.rawQuery("PRAGMA table_info('" + tableName + "')", null)) {
int index = res.getColumnIndex("name");
@ -93,10 +172,530 @@ public class DBHelper {
*
* @return the "WITHOUT ROWID" string or an empty string for pre-Lollipop devices
*/
@NonNull
public static String getWithoutRowId() {
if (GBApplication.isRunningLollipopOrLater()) {
return " WITHOUT ROWID;";
}
return "";
}
/**
* Looks up the user entity in the database. If a user exists already, it will
* be updated with the current preferences values. If no user exists yet, it will
* be created in the database.
*
* Note: so far there is only ever a single user; there is no multi-user support yet
* @param session
* @return the User entity
*/
@NonNull
public static User getUser(DaoSession session) {
ActivityUser prefsUser = new ActivityUser();
UserDao userDao = session.getUserDao();
User user;
List<User> users = userDao.loadAll();
if (users.isEmpty()) {
user = createUser(prefsUser, session);
} else {
user = users.get(0); // TODO: multiple users support?
ensureUserUpToDate(user, prefsUser, session);
}
ensureUserAttributes(user, prefsUser, session);
return user;
}
@NonNull
public static UserAttributes getUserAttributes(User user) {
List<UserAttributes> list = user.getUserAttributesList();
if (list.isEmpty()) {
throw new IllegalStateException("user has no attributes");
}
return list.get(0);
}
@NonNull
private static User createUser(ActivityUser prefsUser, DaoSession session) {
User user = new User();
ensureUserUpToDate(user, prefsUser, session);
return user;
}
private static void ensureUserUpToDate(User user, ActivityUser prefsUser, DaoSession session) {
if (!isUserUpToDate(user, prefsUser)) {
user.setName(prefsUser.getName());
user.setBirthday(prefsUser.getUserBirthday());
user.setGender(prefsUser.getGender());
if (user.getId() == null) {
session.getUserDao().insert(user);
} else {
session.getUserDao().update(user);
}
}
}
public static boolean isUserUpToDate(User user, ActivityUser prefsUser) {
if (!Objects.equals(user.getName(), prefsUser.getName())) {
return false;
}
if (!Objects.equals(user.getBirthday(), prefsUser.getUserBirthday())) {
return false;
}
if (user.getGender() != prefsUser.getGender()) {
return false;
}
return true;
}
private static void ensureUserAttributes(User user, ActivityUser prefsUser, DaoSession session) {
List<UserAttributes> userAttributes = user.getUserAttributesList();
UserAttributes[] previousUserAttributes = new UserAttributes[1];
if (hasUpToDateUserAttributes(userAttributes, prefsUser, previousUserAttributes)) {
return;
}
Calendar now = DateTimeUtils.getCalendarUTC();
invalidateUserAttributes(previousUserAttributes[0], now, session);
UserAttributes attributes = new UserAttributes();
attributes.setValidFromUTC(now.getTime());
attributes.setHeightCM(prefsUser.getHeightCm());
attributes.setWeightKG(prefsUser.getWeightKg());
attributes.setSleepGoalHPD(prefsUser.getSleepDuration());
attributes.setStepsGoalSPD(prefsUser.getStepsGoal());
attributes.setUserId(user.getId());
session.getUserAttributesDao().insert(attributes);
// sort order is important, so we re-fetch from the db
// userAttributes.add(attributes);
user.resetUserAttributesList();
}
private static void invalidateUserAttributes(UserAttributes userAttributes, Calendar now, DaoSession session) {
if (userAttributes != null) {
Calendar invalid = (Calendar) now.clone();
invalid.add(Calendar.MINUTE, -1);
userAttributes.setValidToUTC(invalid.getTime());
session.getUserAttributesDao().update(userAttributes);
}
}
private static boolean hasUpToDateUserAttributes(List<UserAttributes> userAttributes, ActivityUser prefsUser, UserAttributes[] outPreviousUserAttributes) {
for (UserAttributes attr : userAttributes) {
if (!isValidNow(attr)) {
continue;
}
if (isEqual(attr, prefsUser)) {
return true;
} else {
outPreviousUserAttributes[0] = attr;
}
}
return false;
}
// TODO: move this into db queries?
private static boolean isValidNow(ValidByDate element) {
Calendar cal = DateTimeUtils.getCalendarUTC();
Date nowUTC = cal.getTime();
return isValid(element, nowUTC);
}
private static boolean isValid(ValidByDate element, Date nowUTC) {
Date validFromUTC = element.getValidFromUTC();
Date validToUTC = element.getValidToUTC();
if (nowUTC.before(validFromUTC)) {
return false;
}
if (validToUTC != null && nowUTC.after(validToUTC)) {
return false;
}
return true;
}
private static boolean isEqual(UserAttributes attr, ActivityUser prefsUser) {
if (prefsUser.getHeightCm() != attr.getHeightCM()) {
LOG.info("user height changed to " + prefsUser.getHeightCm() + " from " + attr.getHeightCM());
return false;
}
if (prefsUser.getWeightKg() != attr.getWeightKG()) {
LOG.info("user changed to " + prefsUser.getWeightKg() + " from " + attr.getWeightKG());
return false;
}
if (!Integer.valueOf(prefsUser.getSleepDuration()).equals(attr.getSleepGoalHPD())) {
LOG.info("user sleep goal changed to " + prefsUser.getSleepDuration() + " from " + attr.getSleepGoalHPD());
return false;
}
if (!Integer.valueOf(prefsUser.getStepsGoal()).equals(attr.getStepsGoalSPD())) {
LOG.info("user steps goal changed to " + prefsUser.getStepsGoal() + " from " + attr.getStepsGoalSPD());
return false;
}
return true;
}
private static boolean isEqual(DeviceAttributes attr, GBDevice gbDevice) {
if (!Objects.equals(attr.getFirmwareVersion1(), gbDevice.getFirmwareVersion())) {
return false;
}
if (!Objects.equals(attr.getFirmwareVersion2(), gbDevice.getFirmwareVersion2())) {
return false;
}
if (!Objects.equals(attr.getVolatileIdentifier(), gbDevice.getVolatileAddress())) {
return false;
}
return true;
}
public static Device findDevice(GBDevice gbDevice, DaoSession session) {
DeviceDao deviceDao = session.getDeviceDao();
Query<Device> query = deviceDao.queryBuilder().where(DeviceDao.Properties.Identifier.eq(gbDevice.getAddress())).build();
List<Device> devices = query.list();
if (devices.size() > 0) {
return devices.get(0);
}
return null;
}
/**
* Returns all active (that is, not old, archived ones) from the database.
* (currently the active handling is not available)
* @param daoSession
*/
public static List<Device> getActiveDevices(DaoSession daoSession) {
return daoSession.getDeviceDao().loadAll();
}
/**
* Looks up in the database the Device entity corresponding to the GBDevice. If a device
* exists already, it will be updated with the current preferences values. If no device exists
* yet, it will be created in the database.
*
* @param session
* @return the device entity corresponding to the given GBDevice
*/
public static Device getDevice(GBDevice gbDevice, DaoSession session) {
Device device = findDevice(gbDevice, session);
if (device == null) {
device = createDevice(gbDevice, session);
} else {
ensureDeviceUpToDate(device, gbDevice, session);
}
ensureDeviceAttributes(device, gbDevice, session);
return device;
}
@NonNull
public static DeviceAttributes getDeviceAttributes(Device device) {
List<DeviceAttributes> list = device.getDeviceAttributesList();
if (list.isEmpty()) {
throw new IllegalStateException("device has no attributes");
}
return list.get(0);
}
private static void ensureDeviceUpToDate(Device device, GBDevice gbDevice, DaoSession session) {
if (!isDeviceUpToDate(device, gbDevice)) {
device.setIdentifier(gbDevice.getAddress());
device.setName(gbDevice.getName());
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
device.setManufacturer(coordinator.getManufacturer());
device.setType(gbDevice.getType().getKey());
device.setModel(gbDevice.getModel());
if (device.getId() == null) {
session.getDeviceDao().insert(device);
} else {
session.getDeviceDao().update(device);
}
}
}
private static boolean isDeviceUpToDate(Device device, GBDevice gbDevice) {
if (!Objects.equals(device.getIdentifier(), gbDevice.getAddress())) {
return false;
}
if (!Objects.equals(device.getName(), gbDevice.getName())) {
return false;
}
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
if (!Objects.equals(device.getManufacturer(), coordinator.getManufacturer())) {
return false;
}
if (device.getType() != gbDevice.getType().getKey()) {
return false;
}
if (!Objects.equals(device.getModel(), gbDevice.getModel())) {
return false;
}
return true;
}
private static Device createDevice(GBDevice gbDevice, DaoSession session) {
Device device = new Device();
ensureDeviceUpToDate(device, gbDevice, session);
return device;
}
private static void ensureDeviceAttributes(Device device, GBDevice gbDevice, DaoSession session) {
List<DeviceAttributes> deviceAttributes = device.getDeviceAttributesList();
DeviceAttributes[] previousDeviceAttributes = new DeviceAttributes[1];
if (hasUpToDateDeviceAttributes(deviceAttributes, gbDevice, previousDeviceAttributes)) {
return;
}
Calendar now = DateTimeUtils.getCalendarUTC();
invalidateDeviceAttributes(previousDeviceAttributes[0], now, session);
DeviceAttributes attributes = new DeviceAttributes();
attributes.setDeviceId(device.getId());
attributes.setValidFromUTC(now.getTime());
attributes.setFirmwareVersion1(gbDevice.getFirmwareVersion());
attributes.setFirmwareVersion2(gbDevice.getFirmwareVersion2());
attributes.setVolatileIdentifier(gbDevice.getVolatileAddress());
DeviceAttributesDao attributesDao = session.getDeviceAttributesDao();
attributesDao.insert(attributes);
// sort order is important, so we re-fetch from the db
// deviceAttributes.add(attributes);
device.resetDeviceAttributesList();
}
private static void invalidateDeviceAttributes(DeviceAttributes deviceAttributes, Calendar now, DaoSession session) {
if (deviceAttributes != null) {
Calendar invalid = (Calendar) now.clone();
invalid.add(Calendar.MINUTE, -1);
deviceAttributes.setValidToUTC(invalid.getTime());
session.getDeviceAttributesDao().update(deviceAttributes);
}
}
private static boolean hasUpToDateDeviceAttributes(List<DeviceAttributes> deviceAttributes, GBDevice gbDevice, DeviceAttributes[] outPreviousAttributes) {
for (DeviceAttributes attr : deviceAttributes) {
if (!isValidNow(attr)) {
continue;
}
if (isEqual(attr, gbDevice)) {
return true;
} else {
outPreviousAttributes[0] = attr;
}
}
return false;
}
@NonNull
public static List<ActivityDescription> findActivityDecriptions(@NonNull User user, int tsFrom, int tsTo, @NonNull DaoSession session) {
Property tsFromProperty = ActivityDescriptionDao.Properties.TimestampFrom;
Property tsToProperty = ActivityDescriptionDao.Properties.TimestampTo;
Property userIdProperty = ActivityDescriptionDao.Properties.UserId;
QueryBuilder<ActivityDescription> qb = session.getActivityDescriptionDao().queryBuilder();
qb.where(userIdProperty.eq(user.getId()), isAtLeastPartiallyInRange(qb, tsFromProperty, tsToProperty, tsFrom, tsTo));
List<ActivityDescription> descriptions = qb.build().list();
return descriptions;
}
/**
* Returns a condition that matches when the range of the entity (tsFromProperty..tsToProperty)
* is completely or partially inside the range tsFrom..tsTo.
* @param qb the query builder to use
* @param tsFromProperty the property indicating the start of the entity's range
* @param tsToProperty the property indicating the end of the entity's range
* @param tsFrom the timestamp indicating the start of the range to match
* @param tsTo the timestamp indicating the end of the range to match
* @param <T> the query builder's type parameter
* @return the range WhereCondition
*/
private static <T> WhereCondition isAtLeastPartiallyInRange(QueryBuilder<T> qb, Property tsFromProperty, Property tsToProperty, int tsFrom, int tsTo) {
return qb.and(tsFromProperty.lt(tsTo), tsToProperty.gt(tsFrom));
}
@NonNull
public static ActivityDescription createActivityDescription(@NonNull User user, int tsFrom, int tsTo, @NonNull DaoSession session) {
ActivityDescription desc = new ActivityDescription();
desc.setUser(user);
desc.setTimestampFrom(tsFrom);
desc.setTimestampTo(tsTo);
session.getActivityDescriptionDao().insertOrReplace(desc);
return desc;
}
@NonNull
public static Tag getTag(@NonNull User user, @NonNull String name, @NonNull DaoSession session) {
TagDao tagDao = session.getTagDao();
QueryBuilder<Tag> qb = tagDao.queryBuilder();
Query<Tag> query = qb.where(TagDao.Properties.UserId.eq(user.getId()), TagDao.Properties.Name.eq(name)).build();
List<Tag> tags = query.list();
if (tags.size() > 0) {
return tags.get(0);
}
return createTag(user, name, null, session);
}
static Tag createTag(@NonNull User user, @NonNull String name, @Nullable String description, @NonNull DaoSession session) {
Tag tag = new Tag();
tag.setUserId(user.getId());
tag.setName(name);
tag.setDescription(description);
session.getTagDao().insertOrReplace(tag);
return tag;
}
/**
* Returns the old activity database handler if there is any content in that
* db, or null otherwise.
*
* @return the old activity db handler or null
*/
@Nullable
public ActivityDatabaseHandler getOldActivityDatabaseHandler() {
ActivityDatabaseHandler handler = new ActivityDatabaseHandler(context);
if (handler.hasContent()) {
return handler;
}
return null;
}
public void importOldDb(ActivityDatabaseHandler oldDb, GBDevice targetDevice, DBHandler targetDBHandler) {
DaoSession tempSession = targetDBHandler.getDaoMaster().newSession();
try {
importActivityDatabase(oldDb, targetDevice, tempSession);
} finally {
tempSession.clear();
}
}
private boolean isEmpty(DaoSession session) {
long totalSamplesCount = session.getMiBandActivitySampleDao().count();
totalSamplesCount += session.getPebbleHealthActivitySampleDao().count();
return totalSamplesCount == 0;
}
private void importActivityDatabase(ActivityDatabaseHandler oldDbHandler, GBDevice targetDevice, DaoSession session) {
try (SQLiteDatabase oldDB = oldDbHandler.getReadableDatabase()) {
User user = DBHelper.getUser(session);
for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) {
if (coordinator.supports(targetDevice)) {
AbstractSampleProvider<? extends AbstractActivitySample> sampleProvider = (AbstractSampleProvider<? extends AbstractActivitySample>) coordinator.getSampleProvider(targetDevice, session);
importActivitySamples(oldDB, targetDevice, session, sampleProvider, user);
break;
}
}
}
}
private <T extends AbstractActivitySample> void importActivitySamples(SQLiteDatabase fromDb, GBDevice targetDevice, DaoSession targetSession, AbstractSampleProvider<T> sampleProvider, User user) {
if (sampleProvider instanceof PebbleMisfitSampleProvider) {
GB.toast(context, "Migration of old Misfit data is not supported!", Toast.LENGTH_LONG, GB.WARN);
return;
}
String order = "timestamp";
final String where = "provider=" + sampleProvider.getID();
boolean convertActivityTypeToRange = false;
int currentTypeRun, previousTypeRun, currentTimeStamp, currentTypeStartTimeStamp, currentTypeEndTimeStamp;
List<PebbleHealthActivityOverlay> overlayList = new ArrayList<>();
final int BATCH_SIZE = 100000; // 100.000 samples = rougly 20 MB per batch
List<T> newSamples;
if (sampleProvider instanceof PebbleHealthSampleProvider) {
convertActivityTypeToRange = true;
previousTypeRun = ActivitySample.NOT_MEASURED;
currentTypeStartTimeStamp = -1;
currentTypeEndTimeStamp = -1;
} else {
previousTypeRun = currentTypeStartTimeStamp = currentTypeEndTimeStamp = 0;
}
try (Cursor cursor = fromDb.query(TABLE_GBACTIVITYSAMPLES, null, where, null, null, null, order)) {
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);
long deviceId = DBHelper.getDevice(targetDevice, targetSession).getId();
long userId = user.getId();
newSamples = new ArrayList<>(Math.min(BATCH_SIZE, cursor.getCount()));
while (cursor.moveToNext()) {
T newSample = sampleProvider.createActivitySample();
newSample.setProvider(sampleProvider);
newSample.setUserId(userId);
newSample.setDeviceId(deviceId);
currentTimeStamp = cursor.getInt(colTimeStamp);
newSample.setTimestamp(currentTimeStamp);
newSample.setRawIntensity(getNullableInt(cursor, colIntensity, ActivitySample.NOT_MEASURED));
currentTypeRun = getNullableInt(cursor, colType, ActivitySample.NOT_MEASURED);
newSample.setRawKind(currentTypeRun);
if (convertActivityTypeToRange) {
//at the beginning there is no start timestamp
if (currentTypeStartTimeStamp == -1) {
currentTypeStartTimeStamp = currentTypeEndTimeStamp = currentTimeStamp;
previousTypeRun = currentTypeRun;
}
if (currentTypeRun != previousTypeRun) {
//we used not to store the last sample, now we do the opposite and we need to round up
currentTypeEndTimeStamp = currentTimeStamp;
//if the Type has changed, the run has ended. Only store light and deep sleep data
if (previousTypeRun == 4) {
overlayList.add(new PebbleHealthActivityOverlay(currentTypeStartTimeStamp, currentTypeEndTimeStamp, sampleProvider.toRawActivityKind(ActivityKind.TYPE_LIGHT_SLEEP), deviceId, userId, null));
} else if (previousTypeRun == 5) {
overlayList.add(new PebbleHealthActivityOverlay(currentTypeStartTimeStamp, currentTypeEndTimeStamp, sampleProvider.toRawActivityKind(ActivityKind.TYPE_DEEP_SLEEP), deviceId, userId, null));
}
currentTypeStartTimeStamp = currentTimeStamp;
previousTypeRun = currentTypeRun;
} else {
//just expand the run
currentTypeEndTimeStamp = currentTimeStamp;
}
}
newSample.setSteps(getNullableInt(cursor, colSteps, ActivitySample.NOT_MEASURED));
if (colCustomShort > -1) {
newSample.setHeartRate(getNullableInt(cursor, colCustomShort, ActivitySample.NOT_MEASURED));
} else {
newSample.setHeartRate(ActivitySample.NOT_MEASURED);
}
newSamples.add(newSample);
if ((newSamples.size() % BATCH_SIZE) == 0) {
sampleProvider.getSampleDao().insertOrReplaceInTx(newSamples, true);
targetSession.clear();
newSamples.clear();
}
}
// and insert the remaining samples
if (!newSamples.isEmpty()) {
sampleProvider.getSampleDao().insertOrReplaceInTx(newSamples, true);
}
// store the overlay records
if (!overlayList.isEmpty()) {
PebbleHealthActivityOverlayDao overlayDao = targetSession.getPebbleHealthActivityOverlayDao();
overlayDao.insertOrReplaceInTx(overlayList);
}
}
}
private int getNullableInt(Cursor cursor, int columnIndex, int defaultValue) {
if (cursor.isNull(columnIndex)) {
return defaultValue;
}
return cursor.getInt(columnIndex);
}
public static void clearSession() {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
session.clear();
} catch (Exception e) {
LOG.warn("Unable to acquire database to clear the session", e);
}
}
}

View File

@ -0,0 +1,34 @@
package nodomain.freeyourgadget.gadgetbridge.database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.schema.SchemaMigration;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
public class DBOpenHelper extends DaoMaster.OpenHelper {
private final String updaterClassNamePrefix;
private final Context context;
public DBOpenHelper(Context context, String dbName, SQLiteDatabase.CursorFactory factory) {
super(context, dbName, factory);
updaterClassNamePrefix = dbName + "Update_";
this.context = context;
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
DaoMaster.createAllTables(db, true);
new SchemaMigration(updaterClassNamePrefix).onUpgrade(db, oldVersion, newVersion);
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
DaoMaster.createAllTables(db, true);
new SchemaMigration(updaterClassNamePrefix).onDowngrade(db, oldVersion, newVersion);
}
public Context getContext() {
return context;
}
}

View File

@ -1,31 +0,0 @@
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,26 @@
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySampleDao;
/*
* adds heart rate column to health table
*/
public class GadgetbridgeUpdate_14 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
if (!DBHelper.existsColumn(PebbleHealthActivitySampleDao.TABLENAME, PebbleHealthActivitySampleDao.Properties.HeartRate.columnName, db)) {
String ADD_COLUMN_HEART_RATE = "ALTER TABLE " + PebbleHealthActivitySampleDao.TABLENAME + " ADD COLUMN "
+ PebbleHealthActivitySampleDao.Properties.HeartRate.columnName + " INTEGER NOT NULL DEFAULT 0;";
db.execSQL(ADD_COLUMN_HEART_RATE);
}
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
}
}

View File

@ -0,0 +1,26 @@
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
/*
* adds heart rate column to health table
*/
public class GadgetbridgeUpdate_15 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
if (!DBHelper.existsColumn(DeviceAttributesDao.TABLENAME, DeviceAttributesDao.Properties.VolatileIdentifier.columnName, db)) {
String ADD_COLUMN_VOLATILE_IDENTIFIER = "ALTER TABLE " + DeviceAttributesDao.TABLENAME + " ADD COLUMN "
+ DeviceAttributesDao.Properties.VolatileIdentifier.columnName + " TEXT;";
db.execSQL(ADD_COLUMN_VOLATILE_IDENTIFIER);
}
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
}
}

View File

@ -0,0 +1,64 @@
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class SchemaMigration {
private static final Logger LOG = LoggerFactory.getLogger(SchemaMigration.class);
private final String classNamePrefix;
public SchemaMigration(String updaterClassNamePrefix) {
classNamePrefix = updaterClassNamePrefix;
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
LOG.info("ActivityDatabase: schema upgrade requested from " + oldVersion + " to " + newVersion);
try {
for (int i = oldVersion + 1; i <= newVersion; i++) {
DBUpdateScript updater = getUpdateScript(db, i);
if (updater != null) {
LOG.info("upgrading activity database to version " + i);
updater.upgradeSchema(db);
}
}
LOG.info("activity database is now at version " + newVersion);
} catch (RuntimeException ex) {
GB.toast("Error upgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
throw ex; // reject upgrade
}
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
LOG.info("ActivityDatabase: schema downgrade requested from " + oldVersion + " to " + newVersion);
try {
for (int i = oldVersion; i >= newVersion; i--) {
DBUpdateScript updater = getUpdateScript(db, i);
if (updater != null) {
LOG.info("downgrading activity database to version " + (i - 1));
updater.downgradeSchema(db);
}
}
LOG.info("activity database is now at version " + newVersion);
} catch (RuntimeException ex) {
GB.toast("Error downgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
throw ex; // reject downgrade
}
}
private DBUpdateScript getUpdateScript(SQLiteDatabase db, int version) {
try {
Class<?> updateClass = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + "." + classNamePrefix + version);
return (DBUpdateScript) updateClass.newInstance();
} catch (ClassNotFoundException e) {
return null;
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Error instantiating DBUpdate class for version " + version, e);
}
}
}

View File

@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.deviceevents;
public class GBDeviceEventNotificationControl extends GBDeviceEvent {
public int handle;
public String phoneNumber;
public String reply;
public Event event = Event.UNKNOWN;

View File

@ -1,9 +1,106 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanFilter;
import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDeviceCoordinator.class);
@Override
public final boolean supports(GBDeviceCandidate candidate) {
return getSupportedType(candidate).isSupported();
}
@Override
public boolean supports(GBDevice device) {
return getDeviceType().equals(device.getType());
}
@NonNull
@Override
public Collection<? extends ScanFilter> createBLEScanFilters() {
return Collections.emptyList();
}
@Override
public GBDevice createDevice(GBDeviceCandidate candidate) {
return new GBDevice(candidate.getDevice().getAddress(), candidate.getName(), getDeviceType());
}
@Override
public void deleteDevice(final GBDevice gbDevice) throws GBException {
LOG.info("will try to delete device: " + gbDevice.getName());
if (gbDevice.isConnected() || gbDevice.isConnecting()) {
GBApplication.deviceService().disconnect();
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
Device device = DBHelper.findDevice(gbDevice, session);
if (device != null) {
deleteDevice(gbDevice, device, session);
QueryBuilder<?> qb = session.getDeviceAttributesDao().queryBuilder();
qb.where(DeviceAttributesDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities();
session.getDeviceDao().delete(device);
} else {
LOG.info("device to delete not found in db: " + gbDevice);
}
} catch (Exception e) {
throw new GBException("Error deleting device: " + e.getMessage(), e);
}
}
/**
* Hook for subclasses to perform device-specific deletion logic, e.g. db cleanup.
* @param gbDevice the GBDevice
* @param device the corresponding database Device
* @param session the session to use
* @throws GBException
*/
protected abstract void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException;
@Override
public boolean allowFetchActivityData(GBDevice device) {
return device.isInitialized() && !device.isBusy() && supportsActivityDataFetching();
}
public boolean isHealthWearable(BluetoothDevice device) {
BluetoothClass bluetoothClass = device.getBluetoothClass();
if (bluetoothClass == null) {
LOG.warn("unable to determine bluetooth device class of " + device);
return false;
}
if (bluetoothClass.getMajorDeviceClass() == BluetoothClass.Device.Major.WEARABLE
|| bluetoothClass.getMajorDeviceClass() == BluetoothClass.Device.Major.UNCATEGORIZED) {
int deviceClasses =
BluetoothClass.Device.HEALTH_BLOOD_PRESSURE
| BluetoothClass.Device.HEALTH_DATA_DISPLAY
| BluetoothClass.Device.HEALTH_PULSE_RATE
| BluetoothClass.Device.HEALTH_WEIGHING
| BluetoothClass.Device.HEALTH_UNCATEGORIZED
| BluetoothClass.Device.HEALTH_PULSE_OXIMETER
| BluetoothClass.Device.HEALTH_GLUCOSE;
return (bluetoothClass.getDeviceClass() & deviceClasses) != 0;
}
return false;
}
}

View File

@ -0,0 +1,191 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.QueryBuilder;
import de.greenrobot.dao.query.WhereCondition;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
/**
* Base class for all sample providers. A Sample provider is device specific and provides
* access to the device specific samples. There are both read and write operations.
* @param <T> the sample type
*/
public abstract class AbstractSampleProvider<T extends AbstractActivitySample> implements SampleProvider<T> {
private static final WhereCondition[] NO_CONDITIONS = new WhereCondition[0];
private final DaoSession mSession;
private final GBDevice mDevice;
protected AbstractSampleProvider(GBDevice device, DaoSession session) {
mDevice = device;
mSession = session;
}
public GBDevice getDevice() {
return mDevice;
}
public DaoSession getSession() {
return mSession;
}
@Override
public List<T> getAllActivitySamples(int timestamp_from, int timestamp_to) {
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL);
}
@Override
public List<T> getActivitySamples(int timestamp_from, int timestamp_to) {
if (getRawKindSampleProperty() != null) {
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ACTIVITY);
} else {
return getActivitySamplesByActivityFilter(timestamp_from, timestamp_to, ActivityKind.TYPE_ACTIVITY);
}
}
@Override
public List<T> getSleepSamples(int timestamp_from, int timestamp_to) {
if (getRawKindSampleProperty() != null) {
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_SLEEP);
} else {
return getActivitySamplesByActivityFilter(timestamp_from, timestamp_to, ActivityKind.TYPE_SLEEP);
}
}
@Override
public void addGBActivitySample(T activitySample) {
getSampleDao().insertOrReplace(activitySample);
}
@Override
public void addGBActivitySamples(T[] activitySamples) {
getSampleDao().insertOrReplaceInTx(activitySamples);
}
@Nullable
@Override
public T getLatestActivitySample() {
QueryBuilder<T> qb = getSampleDao().queryBuilder();
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
// no device, no sample
return null;
}
Property deviceProperty = getDeviceIdentifierSampleProperty();
qb.where(deviceProperty.eq(dbDevice.getId())).orderDesc(getTimestampSampleProperty()).limit(1);
List<T> samples = qb.build().list();
if (samples.isEmpty()) {
return null;
}
T sample = samples.get(0);
sample.setProvider(this);
return sample;
}
protected List<T> getGBActivitySamples(int timestamp_from, int timestamp_to, int activityType) {
if (getRawKindSampleProperty() == null && activityType != ActivityKind.TYPE_ALL) {
// if we do not have a raw kind property we cannot query anything else then TYPE_ALL
return Collections.emptyList();
}
QueryBuilder<T> qb = getSampleDao().queryBuilder();
Property timestampProperty = getTimestampSampleProperty();
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
// no device, no samples
return Collections.emptyList();
}
Property deviceProperty = getDeviceIdentifierSampleProperty();
qb.where(deviceProperty.eq(dbDevice.getId()), timestampProperty.ge(timestamp_from))
.where(timestampProperty.le(timestamp_to), getClauseForActivityType(qb, activityType));
List<T> samples = qb.build().list();
for (T sample : samples) {
sample.setProvider(this);
}
detachFromSession();
return samples;
}
/**
* Detaches all samples of this type from the session. Changes to them may not be
* written back to the database.
*
* Subclasses should call this method after performing custom queries.
*/
protected void detachFromSession() {
getSampleDao().detachAll();
}
private WhereCondition[] getClauseForActivityType(QueryBuilder qb, int activityTypes) {
if (activityTypes == ActivityKind.TYPE_ALL) {
return NO_CONDITIONS;
}
int[] dbActivityTypes = ActivityKind.mapToDBActivityTypes(activityTypes, this);
WhereCondition activityTypeCondition = getActivityTypeConditions(qb, dbActivityTypes);
return new WhereCondition[] { activityTypeCondition };
}
private WhereCondition getActivityTypeConditions(QueryBuilder qb, int[] dbActivityTypes) {
// What a crappy QueryBuilder API ;-( QueryBuilder.or(WhereCondition[]) with a runtime array length
// check would have worked just fine.
if (dbActivityTypes.length == 0) {
return null;
}
Property rawKindProperty = getRawKindSampleProperty();
if (rawKindProperty == null) {
return null;
}
if (dbActivityTypes.length == 1) {
return rawKindProperty.eq(dbActivityTypes[0]);
}
if (dbActivityTypes.length == 2) {
return qb.or(rawKindProperty.eq(dbActivityTypes[0]),
rawKindProperty.eq(dbActivityTypes[1]));
}
final int offset = 2;
int len = dbActivityTypes.length - offset;
WhereCondition[] trailingConditions = new WhereCondition[len];
for (int i = 0; i < len; i++) {
trailingConditions[i] = rawKindProperty.eq(dbActivityTypes[i + offset]);
}
return qb.or(rawKindProperty.eq(dbActivityTypes[0]),
rawKindProperty.eq(dbActivityTypes[1]),
trailingConditions);
}
private List<T> getActivitySamplesByActivityFilter(int timestamp_from, int timestamp_to, int activityFilter) {
List<T> samples = getAllActivitySamples(timestamp_from, timestamp_to);
List<T> filteredSamples = new ArrayList<>();
for (T sample : samples) {
if ((sample.getKind() & activityFilter) != 0) {
filteredSamples.add(sample);
}
}
return filteredSamples;
}
public abstract AbstractDao<T,?> getSampleDao();
@Nullable
protected abstract Property getRawKindSampleProperty();
@NonNull
protected abstract Property getTimestampSampleProperty();
@NonNull
protected abstract Property getDeviceIdentifierSampleProperty();
}

View File

@ -1,11 +1,20 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import java.util.Collection;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
/**
@ -21,7 +30,18 @@ public interface DeviceCoordinator {
String EXTRA_DEVICE_MAC_ADDRESS = "nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_MAC_ADDRESS";
/**
* Checks whether this candidate handles the given candidate.
* Checks whether this coordinator handles the given candidate.
* Returns the supported device type for the given candidate or
* DeviceType.UNKNOWN
*
* @param candidate
* @return the supported device type for the given candidate.
*/
@NonNull
DeviceType getSupportedType(GBDeviceCandidate candidate);
/**
* Checks whether this coordinator handles the given candidate.
*
* @param candidate
* @return true if this coordinator handles the given candidate.
@ -36,6 +56,24 @@ public interface DeviceCoordinator {
*/
boolean supports(GBDevice device);
/**
* Returns a list of scan filters that shall be used to discover devices supported
* by this coordinator.
* @return the list of scan filters, may be empty
*/
@NonNull
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
Collection<? extends ScanFilter> createBLEScanFilters();
GBDevice createDevice(GBDeviceCandidate candidate);
/**
* Deletes all information, including all related database content about the
* given device.
* @throws GBException
*/
void deleteDevice(GBDevice device) throws GBException;
/**
* Returns the kind of device type this coordinator supports.
*
@ -67,6 +105,14 @@ public interface DeviceCoordinator {
*/
boolean supportsActivityDataFetching();
/**
* Returns true if activity tracking is supported by the device
* (with this coordinator).
*
* @return
*/
boolean supportsActivityTracking();
/**
* Returns true if activity data fetching is supported AND possible at this
* very moment. This will consider the device state (being connected/disconnected/busy...)
@ -82,7 +128,7 @@ public interface DeviceCoordinator {
*
* @return
*/
SampleProvider getSampleProvider();
SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session);
/**
* Finds an install handler for the given uri that can install the given
@ -102,11 +148,36 @@ public interface DeviceCoordinator {
boolean supportsScreenshots();
/**
* Returns true if this device/coordinator supports settig alarms.
* Returns true if this device/coordinator supports setting alarms.
*
* @return
*/
boolean supportsAlarmConfiguration();
/**
* Returns true if the given device supports heart rate measurements.
* @return
*/
boolean supportsHeartRateMeasurement(GBDevice device);
int getTapString();
/**
* Returns the readable name of the manufacturer.
*/
String getManufacturer();
/**
* Returns true if this device/coordinator supports managing device apps.
*
* @return
*/
boolean supportsAppsManagement();
/**
* Returns the Activity class that will be used to manage device apps.
*
* @return
*/
Class<? extends Activity> getAppsManagementActivity();
}

View File

@ -0,0 +1,163 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
/**
* Provides access to the list of devices managed by Gadgetbridge.
* Changes to the devices (e.g. connection state) or the list of devices
* are broadcasted via #ACTION_DEVICE_CHANGED
*/
public class DeviceManager {
private static final Logger LOG = LoggerFactory.getLogger(DeviceManager.class);
public static final String BLUETOOTH_DEVICE_ACTION_ALIAS_CHANGED = "android.bluetooth.device.action.ALIAS_CHANGED";
/**
* Intent action to notify that the list of devices has changed.
*/
public static final String ACTION_DEVICES_CHANGED
= "nodomain.freeyourgadget.gadgetbridge.devices.devicemanager.action.devices_changed";
/**
* Intent action to notify this class that the list of devices shall be refreshed.
*/
public static final String ACTION_REFRESH_DEVICELIST
= "nodomain.freeyourgadget.gadgetbridge.devices.devicemanager.action.set_version";
private final Context context;
/**
* This list is final, it will never be recreated. Only its contents change.
* This allows direct access to the list from ListAdapters.
*/
private final List<GBDevice> deviceList = new ArrayList<>();
private GBDevice selectedDevice = null;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case ACTION_REFRESH_DEVICELIST: // fall through
case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
refreshPairedDevices();
break;
case BluetoothDevice.ACTION_NAME_CHANGED:
case BLUETOOTH_DEVICE_ACTION_ALIAS_CHANGED:
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
String newName = intent.getStringExtra(BluetoothDevice.EXTRA_NAME);
updateDeviceName(device, newName);
break;
case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev.getAddress() != null) {
int index = deviceList.indexOf(dev); // search by address
if (index >= 0) {
deviceList.set(index, dev);
} else {
deviceList.add(dev);
}
}
updateSelectedDevice(dev);
refreshPairedDevices();
break;
}
}
};
public DeviceManager(Context context) {
this.context = context;
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(DeviceManager.ACTION_REFRESH_DEVICELIST);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
filterLocal.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
LocalBroadcastManager.getInstance(context).registerReceiver(mReceiver, filterLocal);
IntentFilter filterGlobal = new IntentFilter();
filterGlobal.addAction(BluetoothDevice.ACTION_NAME_CHANGED);
filterGlobal.addAction(BLUETOOTH_DEVICE_ACTION_ALIAS_CHANGED);
filterGlobal.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
context.registerReceiver(mReceiver, filterGlobal);
refreshPairedDevices();
}
private void updateDeviceName(BluetoothDevice device, String newName) {
for (GBDevice dev : deviceList) {
if (device.getAddress().equals(dev.getAddress())) {
if (!dev.getName().equals(newName)) {
dev.setName(newName);
notifyDevicesChanged();
return;
}
}
}
}
private void updateSelectedDevice(GBDevice dev) {
if (selectedDevice == null) {
selectedDevice = dev;
} else {
if (selectedDevice.equals(dev)) {
selectedDevice = dev; // equality vs identity!
} else {
if (selectedDevice.isConnected() && dev.isConnected()) {
LOG.warn("multiple connected devices -- this is currently not really supported");
selectedDevice = dev; // use the last one that changed
} else if (!selectedDevice.isConnected()) {
selectedDevice = dev; // use the last one that changed
}
}
}
}
private void refreshPairedDevices() {
Set<GBDevice> availableDevices = DeviceHelper.getInstance().getAvailableDevices(context);
deviceList.retainAll(availableDevices);
for (GBDevice availableDevice : availableDevices) {
if (!deviceList.contains(availableDevice)) {
deviceList.add(availableDevice);
}
}
Collections.sort(deviceList, new Comparator<GBDevice>() {
@Override
public int compare(GBDevice lhs, GBDevice rhs) {
return Collator.getInstance().compare(lhs.getName(), rhs.getName());
}
});
notifyDevicesChanged();
}
/**
* The returned list is final, it will never be recreated. Only its contents change.
* This allows direct access to the list from ListAdapters.
*/
public List<GBDevice> getDevices() {
return Collections.unmodifiableList(deviceList);
}
@Nullable
public GBDevice getSelectedDevice() {
return selectedDevice;
}
private void notifyDevicesChanged() {
LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(ACTION_DEVICES_CHANGED));
}
}

View File

@ -8,12 +8,13 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
/**
* Specifies all events that GadgetBridge intends to send to the gadget device.
* Specifies all events that Gadgetbridge intends to send to the gadget device.
* Implementations can decide to ignore events that they do not support.
* Implementations need to send/encode event to the connected device.
*/
@ -26,6 +27,8 @@ public interface EventHandler {
void onSetCallState(CallSpec callSpec);
void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec);
void onSetMusicState(MusicStateSpec stateSpec);
void onSetMusicInfo(MusicSpec musicSpec);
@ -42,6 +45,8 @@ public interface EventHandler {
void onAppConfiguration(UUID appUuid, String config);
void onAppReorder(UUID uuids[]);
void onFetchActivityData();
void onReboot();
@ -52,6 +57,8 @@ public interface EventHandler {
void onFindDevice(boolean start);
void onSetConstantVibration(int integer);
void onScreenshotReq();
void onEnableHeartRateSleepSupport(boolean enable);
@ -59,4 +66,13 @@ public interface EventHandler {
void onAddCalendarEvent(CalendarEventSpec calendarEventSpec);
void onDeleteCalendarEvent(byte type, long id);
/**
* Sets the given option in the device, typically with values from the preferences.
* The config name is device specific.
* @param config the device specific option to set on the device
*/
void onSendConfiguration(String config);
void onTestNewFunction();
}

View File

@ -1,13 +1,39 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
public interface SampleProvider {
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
/**
* Interface to retrieve samples from the database, and also create and add samples to the database.
* There are multiple device specific implementations, this interface defines the generic access.
*
* Note that the provided samples must typically be considered read-only, because they are immediately
* removed from the session before they are returned.
*
* @param <T> the device/provider specific sample type (must extend AbstractActivitySample)
*/
public interface SampleProvider<T extends AbstractActivitySample> {
// TODO: these constants can all be removed
int PROVIDER_MIBAND = 0;
int PROVIDER_PEBBLE_MORPHEUZ = 1;
int PROVIDER_PEBBLE_GADGETBRIDGE = 2;
int PROVIDER_PEBBLE_GADGETBRIDGE = 2; // removed
int PROVIDER_PEBBLE_MISFIT = 3;
int PROVIDER_PEBBLE_HEALTH = 4;
int PROVIDER_MIBAND2 = 5;
int PROVIDER_UNKNOWN = 100;
// TODO: can also be removed
/**
* Returns the "id" of this sample provider, as used in Gadgetbridge versions < 0.12.0.
* Only used for importing old samples.
* @deprecated
*/
int getID();
int normalizeType(int rawType);
@ -15,5 +41,59 @@ public interface SampleProvider {
float normalizeIntensity(int rawIntensity);
int getID();
/**
* Returns the list of all samples, of any type, within the given time span.
* @param timestamp_from the start timestamp
* @param timestamp_to the end timestamp
* @return the list of samples of any type
*/
@NonNull
List<T> getAllActivitySamples(int timestamp_from, int timestamp_to);
/**
* Returns the list of all samples that represent user "activity", within
* the given time span. This excludes samples of type sleep, for example.
* @param timestamp_from the start timestamp
* @param timestamp_to the end timestamp
* @return the list of samples of type user activity, e.g. non-sleep
*/
@NonNull
List<T> getActivitySamples(int timestamp_from, int timestamp_to);
/**
* Returns the list of all samples that represent "sleeping", within the
* given time span.
* @param timestamp_from the start timestamp
* @param timestamp_to the end timestamp
* @return the list of samples of type sleep
*/
@NonNull
List<T> getSleepSamples(int timestamp_from, int timestamp_to);
/**
* Adds the given sample to the database. An existing sample with the same
* timestamp will be overwritten.
* @param activitySample the sample to add
*/
void addGBActivitySample(T activitySample);
/**
* Adds the given samples to the database. Existing samples with the same
* timestamp will be overwritten.
* @param activitySamples the samples to add
*/
void addGBActivitySamples(T[] activitySamples);
/**
* Factory method to creates an empty sample of the correct type for this sample provider
* @return the newly created "empty" sample
*/
T createActivitySample();
/**
* Returns the activity sample with the highest timestamp. or null if none
* @return the latest sample or null
*/
@Nullable
T getLatestActivitySample();
}

View File

@ -3,8 +3,16 @@ package nodomain.freeyourgadget.gadgetbridge.devices;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
@ -29,6 +37,40 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return 0;
}
@Override
public List getAllActivitySamples(int timestamp_from, int timestamp_to) {
return null;
}
@Override
public List getActivitySamples(int timestamp_from, int timestamp_to) {
return null;
}
@Override
public List getSleepSamples(int timestamp_from, int timestamp_to) {
return null;
}
@Override
public void addGBActivitySample(AbstractActivitySample activitySample) {
}
@Override
public void addGBActivitySamples(AbstractActivitySample[] activitySamples) {
}
@Override
public AbstractActivitySample createActivitySample() {
return null;
}
@Nullable
@Override
public AbstractActivitySample getLatestActivitySample() {
return null;
}
@Override
public int getID() {
return PROVIDER_UNKNOWN;
@ -40,13 +82,12 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
}
@Override
public boolean supports(GBDeviceCandidate candidate) {
return false;
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
return DeviceType.UNKNOWN;
}
@Override
public boolean supports(GBDevice device) {
return getDeviceType().equals(device.getType());
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
@ -65,8 +106,8 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
}
@Override
public SampleProvider getSampleProvider() {
return sampleProvider;
public SampleProvider<?> getSampleProvider(GBDevice device, DaoSession session) {
return new UnknownSampleProvider();
}
@Override
@ -79,6 +120,11 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsActivityTracking() {
return false;
}
@Override
public boolean supportsScreenshots() {
return false;
@ -89,8 +135,28 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;
}
@Override
public int getTapString() {
return 0;
}
@Override
public String getManufacturer() {
return "unknown";
}
@Override
public boolean supportsAppsManagement() {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
}

View File

@ -0,0 +1,110 @@
package nodomain.freeyourgadget.gadgetbridge.devices.liveview;
//Changed by Renze: Fixed brightness constants
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* Message constants reverse-engineered by Andrew de Quincey (<a
* href="http://adq.livejournal.com">http://adq.livejournal.com</a>).
*
* @author Robert &lt;xperimental@solidproject.de&gt;
*/
public final class LiveviewConstants {
public static Charset ENCODING = StandardCharsets.ISO_8859_1;
public static ByteOrder BYTE_ORDER = ByteOrder.BIG_ENDIAN;
public static final byte CLOCK_24H = 0;
public static final byte CLOCK_12H = 1;
public static final byte MSG_GETCAPS = 1;
public static final byte MSG_GETCAPS_RESP = 2;
public static final byte MSG_DISPLAYTEXT = 3;
public static final byte MSG_DISPLAYTEXT_ACK = 4;
public static final byte MSG_DISPLAYPANEL = 5;
public static final byte MSG_DISPLAYPANEL_ACK = 6;
public static final byte MSG_DEVICESTATUS = 7;
public static final byte MSG_DEVICESTATUS_ACK = 8;
public static final byte MSG_DISPLAYBITMAP = 19;
public static final byte MSG_DISPLAYBITMAP_ACK = 20;
public static final byte MSG_CLEARDISPLAY = 21;
public static final byte MSG_CLEARDISPLAY_ACK = 22;
public static final byte MSG_SETMENUSIZE = 23;
public static final byte MSG_SETMENUSIZE_ACK = 24;
public static final byte MSG_GETMENUITEM = 25;
public static final byte MSG_GETMENUITEM_RESP = 26;
public static final byte MSG_GETALERT = 27;
public static final byte MSG_GETALERT_RESP = 28;
public static final byte MSG_NAVIGATION = 29;
public static final byte MSG_NAVIGATION_RESP = 30;
public static final byte MSG_SETSTATUSBAR = 33;
public static final byte MSG_SETSTATUSBAR_ACK = 34;
public static final byte MSG_GETMENUITEMS = 35;
public static final byte MSG_SETMENUSETTINGS = 36;
public static final byte MSG_SETMENUSETTINGS_ACK = 37;
public static final byte MSG_GETTIME = 38;
public static final byte MSG_GETTIME_RESP = 39;
public static final byte MSG_SETLED = 40;
public static final byte MSG_SETLED_ACK = 41;
public static final byte MSG_SETVIBRATE = 42;
public static final byte MSG_SETVIBRATE_ACK = 43;
public static final byte MSG_ACK = 44;
public static final byte MSG_SETSCREENMODE = 64;
public static final byte MSG_SETSCREENMODE_ACK = 65;
public static final byte MSG_GETSCREENMODE = 66;
public static final byte MSG_GETSCREENMODE_RESP = 67;
public static final int DEVICESTATUS_OFF = 0;
public static final int DEVICESTATUS_ON = 1;
public static final int DEVICESTATUS_MENU = 2;
public static final byte RESULT_OK = 0;
public static final byte RESULT_ERROR = 1;
public static final byte RESULT_OOM = 2;
public static final byte RESULT_EXIT = 3;
public static final byte RESULT_CANCEL = 4;
public static final int NAVACTION_PRESS = 0;
public static final int NAVACTION_LONGPRESS = 1;
public static final int NAVACTION_DOUBLEPRESS = 2;
public static final int NAVTYPE_UP = 0;
public static final int NAVTYPE_DOWN = 1;
public static final int NAVTYPE_LEFT = 2;
public static final int NAVTYPE_RIGHT = 3;
public static final int NAVTYPE_SELECT = 4;
public static final int NAVTYPE_MENUSELECT = 5;
public static final int ALERTACTION_CURRENT = 0;
public static final int ALERTACTION_FIRST = 1;
public static final int ALERTACTION_LAST = 2;
public static final int ALERTACTION_NEXT = 3;
public static final int ALERTACTION_PREV = 4;
public static final int BRIGHTNESS_OFF = 49;
public static final int BRIGHTNESS_DIM = 50;
public static final int BRIGHTNESS_MAX = 51;
public static final String CLIENT_SOFTWARE_VERSION = "0.0.3";
}

View File

@ -0,0 +1,105 @@
package nodomain.freeyourgadget.gadgetbridge.devices.liveview;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class LiveviewCoordinator extends AbstractDeviceCoordinator {
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
if (name != null && name.startsWith("LiveView")) {
return DeviceType.LIVEVIEW;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.LIVEVIEW;
}
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public Class<? extends Activity> getPrimaryActivity() {
return null;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsActivityDataFetching() {
return false;
}
@Override
public boolean supportsActivityTracking() {
return false;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
}
@Override
public boolean supportsScreenshots() {
return false;
}
@Override
public boolean supportsAlarmConfiguration() {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;
}
@Override
public int getTapString() {
//TODO: changeme
return R.string.tap_connected_device_for_activity;
}
@Override
public String getManufacturer() {
return "Sony Ericsson";
}
@Override
public boolean supportsAppsManagement() {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
// nothing to delete, yet
}
}

View File

@ -0,0 +1,57 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import android.support.annotation.NonNull;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
/**
* Base class for Mi1 and Mi2 sample providers. At the moment they both share the
* same activity sample class.
*/
public abstract class AbstractMiBandSampleProvider extends AbstractSampleProvider<MiBandActivitySample> {
// maybe this should be configurable 256 seems way off, though.
private final float movementDivisor = 180.0f; //256.0f;
public AbstractMiBandSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / movementDivisor;
}
@Override
public AbstractDao<MiBandActivitySample, ?> getSampleDao() {
return getSession().getMiBandActivitySampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return MiBandActivitySampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return MiBandActivitySampleDao.Properties.DeviceId;
}
@Override
protected Property getRawKindSampleProperty() {
return MiBandActivitySampleDao.Properties.RawKind;
}
@Override
public MiBandActivitySample createActivitySample() {
return new MiBandActivitySample();
}
}

View File

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

View File

@ -0,0 +1,107 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelUuid;
import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class MiBand2Coordinator extends MiBandCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(MiBand2Coordinator.class);
@Override
public DeviceType getDeviceType() {
return DeviceType.MIBAND2;
}
@NonNull
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public Collection<? extends ScanFilter> createBLEScanFilters() {
ParcelUuid mi2Service = new ParcelUuid(MiBandService.UUID_SERVICE_MIBAND2_SERVICE);
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(mi2Service).build();
return Collections.singletonList(filter);
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
if (candidate.supportsService(MiBand2Service.UUID_SERVICE_MIBAND2_SERVICE)) {
return DeviceType.MIBAND2;
}
// and a heuristic for now
try {
BluetoothDevice device = candidate.getDevice();
if (isHealthWearable(device)) {
String name = device.getName();
if (name != null && name.equalsIgnoreCase(MiBandConst.MI_BAND2_NAME)) {
return DeviceType.MIBAND2;
}
}
} catch (Exception ex) {
LOG.error("unable to check device support", ex);
}
return DeviceType.UNKNOWN;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
}
@Override
public boolean supportsAlarmConfiguration() {
return true;
}
@Override
public boolean supportsActivityDataFetching() {
return true;
}
@Override
public SampleProvider<? extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new MiBand2SampleProvider(device, session);
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null; // not supported at the moment
}
public static DateTimeDisplay getDateDisplay(Context context) throws IllegalArgumentException {
Prefs prefs = GBApplication.getPrefs();
String dateFormatTime = context.getString(R.string.p_dateformat_time);
if (dateFormatTime.equals(prefs.getString(MiBandConst.PREF_MI2_DATEFORMAT, dateFormatTime))) {
return DateTimeDisplay.TIME;
}
return DateTimeDisplay.DATE_TIME;
}
public static boolean getActivateDisplayOnLiftWrist() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getBoolean(MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT, true);
}
}

View File

@ -0,0 +1,138 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class MiBand2SampleProvider extends AbstractMiBandSampleProvider {
// these come from Mi1
// public static final int TYPE_LIGHT_SLEEP = 5;
// public static final int TYPE_ACTIVITY = -1;
// public static final int TYPE_UNKNOWN = -1;
// public static final int TYPE_NONWEAR = 3;
// public static final int TYPE_CHARGING = 6;
// observed the following values so far:
// 00 01 02 09 0a 0b 0c 10 11
// 0 = same activity kind as before
// 1 = light activity walking?
// 3 = definitely non-wear
// 9 = probably light sleep, definitely some kind of sleep
// 10 = ignore, except for hr (if valid)
// 11 = probably deep sleep
// 12 = definitely wake up
// 17 = definitely not sleep related
public static final int TYPE_UNSET = -1;
public static final int TYPE_NO_CHANGE = 0;
public static final int TYPE_ACTIVITY = 1;
public static final int TYPE_NONWEAR = 3;
public static final int TYPE_CHARGING = 6;
public static final int TYPE_LIGHT_SLEEP = 9;
public static final int TYPE_DEEP_SLEEP = 11;
public static final int TYPE_WAKE_UP = 12;
// appears to be a measurement problem resulting in type = 10 and intensity = 20, at least with fw 1.0.0.39
public static final int TYPE_IGNORE = 10;
public MiBand2SampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public int getID() {
return SampleProvider.PROVIDER_MIBAND2;
}
@Override
protected List<MiBandActivitySample> getGBActivitySamples(int timestamp_from, int timestamp_to, int activityType) {
List<MiBandActivitySample> samples = super.getGBActivitySamples(timestamp_from, timestamp_to, activityType);
postprocess(samples);
return samples;
}
/**
* "Temporary" runtime post processing of activity kinds.
* @param samples
*/
private void postprocess(List<MiBandActivitySample> samples) {
if (samples.isEmpty()) {
return;
}
int lastValidKind = determinePreviousValidActivityType(samples.get(0));
for (MiBandActivitySample sample : samples) {
int rawKind = sample.getRawKind();
switch (rawKind) {
case TYPE_IGNORE:
case TYPE_NO_CHANGE:
if (lastValidKind != TYPE_UNSET) {
sample.setRawKind(lastValidKind);
}
break;
default:
lastValidKind = rawKind;
break;
}
}
}
private int determinePreviousValidActivityType(MiBandActivitySample sample) {
QueryBuilder<MiBandActivitySample> qb = getSampleDao().queryBuilder();
qb.where(MiBandActivitySampleDao.Properties.DeviceId.eq(sample.getDeviceId()),
MiBandActivitySampleDao.Properties.UserId.eq(sample.getUserId()),
MiBandActivitySampleDao.Properties.Timestamp.lt(sample.getTimestamp()),
MiBandActivitySampleDao.Properties.RawKind.notIn(TYPE_IGNORE, TYPE_NO_CHANGE));
qb.limit(1);
List<MiBandActivitySample> result = qb.build().list();
if (result.size() > 0) {
return result.get(0).getRawKind();
}
return TYPE_UNSET;
}
@Override
public int normalizeType(int rawType) {
switch (rawType) {
case TYPE_DEEP_SLEEP:
return ActivityKind.TYPE_DEEP_SLEEP;
case TYPE_LIGHT_SLEEP:
return ActivityKind.TYPE_LIGHT_SLEEP;
case TYPE_ACTIVITY:
return ActivityKind.TYPE_ACTIVITY;
case TYPE_NONWEAR:
return ActivityKind.TYPE_NOT_WORN;
case TYPE_CHARGING:
return ActivityKind.TYPE_NOT_WORN; //I believe it's a safe assumption
case TYPE_IGNORE:
default:
case TYPE_UNSET: // fall through
return ActivityKind.TYPE_UNKNOWN;
}
}
@Override
public int toRawActivityKind(int activityKind) {
switch (activityKind) {
case ActivityKind.TYPE_ACTIVITY:
return TYPE_ACTIVITY;
case ActivityKind.TYPE_DEEP_SLEEP:
return TYPE_DEEP_SLEEP;
case ActivityKind.TYPE_LIGHT_SLEEP:
return TYPE_LIGHT_SLEEP;
case ActivityKind.TYPE_NOT_WORN:
return TYPE_NONWEAR;
case ActivityKind.TYPE_UNKNOWN: // fall through
default:
return TYPE_UNSET;
}
}
}

View File

@ -0,0 +1,347 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport.BASE_UUID;
public class MiBand2Service {
public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0"));
public static final UUID UUID_SERVICE_MIBAND2_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE1"));
public static final UUID UUID_SERVICE_HEART_RATE = UUID.fromString(String.format(BASE_UUID, "180D"));
public static final UUID UUID_SERVICE_WEIGHT_SERVICE = UUID.fromString("00001530-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC0 = UUID.fromString("00000000-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC1 = UUID.fromString("00000001-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC2 = UUID.fromString("00000002-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC3 = UUID.fromString("00000003-0000-3512-2118-0009af100700"); // Alarm related
public static final UUID UUID_UNKNOWN_CHARACTERISTIC4 = UUID.fromString("00000004-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_ACTIVITY_DATA = UUID.fromString("00000005-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC6 = UUID.fromString("00000006-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC7 = UUID.fromString("00000007-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC8 = UUID.fromString("00000008-0000-3512-2118-0009af100700");
// service uuid fee1
public static final UUID UUID_CHARACTERISTIC_AUTH = UUID.fromString("00000009-0000-3512-2118-0009af100700");
public static final UUID UUID_UNKNOWN_CHARACTERISTIC10 = UUID.fromString("00000010-0000-3512-2118-0009af100700");
public static final int ALERT_LEVEL_NONE = 0;
public static final int ALERT_LEVEL_MESSAGE = 1;
public static final int ALERT_LEVEL_PHONE_CALL = 2;
public static final int ALERT_LEVEL_VIBRATE_ONLY = 3;
// set metric distance
// set 12 hour time mode
// public static final UUID UUID_CHARACTERISTIC_DEVICE_INFO = UUID.fromString(String.format(BASE_UUID, "FF01"));
//
// public static final UUID UUID_CHARACTERISTIC_DEVICE_NAME = UUID.fromString(String.format(BASE_UUID, "FF02"));
//
// public static final UUID UUID_CHARACTERISTIC_NOTIFICATION = UUID.fromString(String.format(BASE_UUID, "FF03"));
//
// public static final UUID UUID_CHARACTERISTIC_USER_INFO = UUID.fromString(String.format(BASE_UUID, "FF04"));
//
// public static final UUID UUID_CHARACTERISTIC_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID, "FF05"));
//
// public static final UUID UUID_CHARACTERISTIC_REALTIME_STEPS = UUID.fromString(String.format(BASE_UUID, "FF06"));
//
// public static final UUID UUID_CHARACTERISTIC_ACTIVITY_DATA = UUID.fromString(String.format(BASE_UUID, "FF07"));
//
// public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString(String.format(BASE_UUID, "FF08"));
//
// public static final UUID UUID_CHARACTERISTIC_LE_PARAMS = UUID.fromString(String.format(BASE_UUID, "FF09"));
//
// public static final UUID UUID_CHARACTERISTIC_DATE_TIME = UUID.fromString(String.format(BASE_UUID, "FF0A"));
//
// public static final UUID UUID_CHARACTERISTIC_STATISTICS = UUID.fromString(String.format(BASE_UUID, "FF0B"));
//
// public static final UUID UUID_CHARACTERISTIC_BATTERY = UUID.fromString(String.format(BASE_UUID, "FF0C"));
//
// public static final UUID UUID_CHARACTERISTIC_TEST = UUID.fromString(String.format(BASE_UUID, "FF0D"));
//
// public static final UUID UUID_CHARACTERISTIC_SENSOR_DATA = UUID.fromString(String.format(BASE_UUID, "FF0E"));
//
// public static final UUID UUID_CHARACTERISTIC_PAIR = UUID.fromString(String.format(BASE_UUID, "FF0F"));
//
// public static final UUID UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID, "2A39"));
// public static final UUID UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT = UUID.fromString(String.format(BASE_UUID, "2A37"));
//
//
//
// /* FURTHER UUIDS that were mixed with the other params below. The base UUID for these is unknown */
//
// public static final byte ALIAS_LEN = 0xa;
//
// /*NOTIFICATIONS: usually received on the UUID_CHARACTERISTIC_NOTIFICATION characteristic */
//
// public static final byte NOTIFY_NORMAL = 0x0;
//
// public static final byte NOTIFY_FIRMWARE_UPDATE_FAILED = 0x1;
//
// public static final byte NOTIFY_FIRMWARE_UPDATE_SUCCESS = 0x2;
//
// public static final byte NOTIFY_CONN_PARAM_UPDATE_FAILED = 0x3;
//
// public static final byte NOTIFY_CONN_PARAM_UPDATE_SUCCESS = 0x4;
//
// public static final byte NOTIFY_AUTHENTICATION_SUCCESS = 0x5;
//
// public static final byte NOTIFY_AUTHENTICATION_FAILED = 0x6;
//
// public static final byte NOTIFY_FITNESS_GOAL_ACHIEVED = 0x7;
//
// public static final byte NOTIFY_SET_LATENCY_SUCCESS = 0x8;
//
// public static final byte NOTIFY_RESET_AUTHENTICATION_FAILED = 0x9;
//
// public static final byte NOTIFY_RESET_AUTHENTICATION_SUCCESS = 0xa;
//
// public static final byte NOTIFY_FW_CHECK_FAILED = 0xb;
//
// public static final byte NOTIFY_FW_CHECK_SUCCESS = 0xc;
//
// public static final byte NOTIFY_STATUS_MOTOR_NOTIFY = 0xd;
//
// public static final byte NOTIFY_STATUS_MOTOR_CALL = 0xe;
//
// public static final byte NOTIFY_STATUS_MOTOR_DISCONNECT = 0xf;
//
// public static final byte NOTIFY_STATUS_MOTOR_SMART_ALARM = 0x10;
//
// public static final byte NOTIFY_STATUS_MOTOR_ALARM = 0x11;
//
// public static final byte NOTIFY_STATUS_MOTOR_GOAL = 0x12;
//
// public static final byte NOTIFY_STATUS_MOTOR_AUTH = 0x13;
//
// public static final byte NOTIFY_STATUS_MOTOR_SHUTDOWN = 0x14;
//
// public static final byte NOTIFY_STATUS_MOTOR_AUTH_SUCCESS = 0x15;
//
// public static final byte NOTIFY_STATUS_MOTOR_TEST = 0x16;
//
// // 0x18 is returned when we cancel data sync, perhaps is an ack for this message
//
// public static final byte NOTIFY_UNKNOWN = -0x1;
//
// public static final int NOTIFY_PAIR_CANCEL = 0xef;
//
// public static final int NOTIFY_DEVICE_MALFUNCTION = 0xff;
//
//
// /* MESSAGES: unknown */
//
// public static final byte MSG_CONNECTED = 0x0;
//
// public static final byte MSG_DISCONNECTED = 0x1;
//
// public static final byte MSG_CONNECTION_FAILED = 0x2;
//
// public static final byte MSG_INITIALIZATION_FAILED = 0x3;
//
// public static final byte MSG_INITIALIZATION_SUCCESS = 0x4;
//
// public static final byte MSG_STEPS_CHANGED = 0x5;
//
// public static final byte MSG_DEVICE_STATUS_CHANGED = 0x6;
//
// public static final byte MSG_BATTERY_STATUS_CHANGED = 0x7;
//
// /* COMMANDS: usually sent to UUID_CHARACTERISTIC_CONTROL_POINT characteristic */
//
// public static final byte COMMAND_SET_TIMER = 0x4;
//
// public static final byte COMMAND_SET_FITNESS_GOAL = 0x5;
//
// public static final byte COMMAND_FETCH_DATA = 0x6;
//
// public static final byte COMMAND_SEND_FIRMWARE_INFO = 0x7;
//
// public static final byte COMMAND_SEND_NOTIFICATION = 0x8;
//
// public static final byte COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE = 0xa;
//
// public static final byte COMMAND_SYNC = 0xb;
//
// public static final byte COMMAND_REBOOT = 0xc;
//
// public static final byte COMMAND_SET_WEAR_LOCATION = 0xf;
//
// public static final byte COMMAND_STOP_SYNC_DATA = 0x11;
//
// public static final byte COMMAND_STOP_MOTOR_VIBRATE = 0x13;
//
// public static final byte COMMAND_SET_REALTIME_STEPS_NOTIFICATION = 0x3;
//
// public static final byte COMMAND_SET_REALTIME_STEP = 0x10;
//
// // Test HR
// public static final byte COMMAND_SET_HR_SLEEP = 0x0;
// public static final byte COMMAND_SET__HR_CONTINUOUS = 0x1;
// public static final byte COMMAND_SET_HR_MANUAL = 0x2;
//
//
// /* FURTHER COMMANDS: unchecked therefore left commented
//
//
// public static final byte COMMAND_FACTORY_RESET = 0x9t;
//
// public static final int COMMAND_SET_COLOR_THEME = et;
//
// public static final byte COMMAND_GET_SENSOR_DATA = 0x12t
//
// */
//
// /* CONNECTION: unknown
//
// public static final CONNECTION_LATENCY_LEVEL_LOW = 0x0t;
//
// public static final CONNECTION_LATENCY_LEVEL_MEDIUM = 0x1t;
//
// public static final CONNECTION_LATENCY_LEVEL_HIGH = 0x2t;
//
// */
//
// /* MODES: probably related to the sample data structure
// */
//
// public static final byte MODE_REGULAR_DATA_LEN_BYTE = 0x0;
//
// // was MODE_REGULAR_DATA_LEN_MINITE
// public static final byte MODE_REGULAR_DATA_LEN_MINUTE = 0x1;
//
// /* PROFILE: unknown
//
// public static final PROFILE_STATE_UNKNOWN:I = 0x0
//
// public static final PROFILE_STATE_INITIALIZATION_SUCCESS:I = 0x1
//
// public static final PROFILE_STATE_INITIALIZATION_FAILED:I = 0x2
//
// public static final PROFILE_STATE_AUTHENTICATION_SUCCESS:I = 0x3
//
// public static final PROFILE_STATE_AUTHENTICATION_FAILED:I = 0x4
//
// */
//
// // TEST_*: sent to UUID_CHARACTERISTIC_TEST characteristic
//
// public static final byte TEST_DISCONNECTED_REMINDER = 0x5;
//
// public static final byte TEST_NOTIFICATION = 0x3;
//
// public static final byte TEST_REMOTE_DISCONNECT = 0x1;
//
// public static final byte TEST_SELFTEST = 0x2;
private static final Map<UUID, String> MIBAND_DEBUG;
/**
* Mi Band 2 authentication has three steps.
* This is step 1: sending a "secret" key to the band.
* This is byte 0, followed by {@link #AUTH_BYTE} and then the key.
* In the response, it is byte 1 in the byte[] value.
*/
public static final byte AUTH_SEND_KEY = 0x01;
/**
* Mi Band 2 authentication has three steps.
* This is step 2: requesting a random authentication key from the band.
* This is byte 0, followed by {@link #AUTH_BYTE}.
* In the response, it is byte 1 in the byte[] value.
*/
public static final byte AUTH_REQUEST_RANDOM_AUTH_NUMBER = 0x02;
/**
* Mi Band 2 authentication has three steps.
* This is step 3: sending the encrypted random authentication key to the band.
* This is byte 0, followed by {@link #AUTH_BYTE} and then the encrypted random authentication key.
* In the response, it is byte 1 in the byte[] value.
*/
public static final byte AUTH_SEND_ENCRYPTED_AUTH_NUMBER = 0x03;
/**
* Received in response to any authentication requests (byte 0 in the byte[] value.
*/
public static final byte AUTH_RESPONSE = 0x10;
/**
* Received in response to any authentication requests (byte 2 in the byte[] value.
* 0x01 means success.
*/
public static final byte AUTH_SUCCESS = 0x01;
/**
* Received in response to any authentication requests (byte 2 in the byte[] value.
* 0x04 means failure.
*/
public static final byte AUTH_FAIL = 0x04;
/**
* In some logs it's 0x0...
*/
public static final byte AUTH_BYTE = 0x8;
// maybe not really activity data, but steps?
public static final byte COMMAND_FETCH_ACTIVITY_DATA = 0x02;
public static final byte[] COMMAND_SET_FITNESS_GOAL_START = new byte[] { 0x10, 0x0, 0x0 };
public static final byte[] COMMAND_SET_FITNESS_GOAL_END = new byte[] { 0, 0 };
public static byte COMMAND_DATEFORMAT = 0x06;
public static final byte[] DATEFORMAT_DATE_TIME = new byte[] { COMMAND_DATEFORMAT, 0x0a, 0x0, 0x03 };
public static final byte[] DATEFORMAT_TIME = new byte[] { COMMAND_DATEFORMAT, 0x0a, 0x0, 0x0 };
public static final byte RESPONSE = 0x10;
public static final byte SUCCESS = 0x01;
public static final byte COMMAND_ACTIVITY_DATA_START_DATE = 0x01;
public static final byte[] RESPONSE_FINISH_SUCCESS = new byte[] {RESPONSE, 2, SUCCESS };
/**
* Received in response to any dateformat configuration request (byte 0 in the byte[] value.
*/
public static final byte[] RESPONSE_DATEFORMAT_SUCCESS = new byte[] { RESPONSE, COMMAND_DATEFORMAT, 0x0a, 0x0, 0x01 };
public static final byte[] RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS = new byte[] { RESPONSE, COMMAND_ACTIVITY_DATA_START_DATE, SUCCESS};
public static final byte[] WEAR_LOCATION_LEFT_WRIST = new byte[] { 0x20, 0x00, 0x00, 0x02 };
public static final byte[] WEAR_LOCATION_RIGHT_WRIST = new byte[] { 0x20, 0x00, 0x00, (byte) 0x82};
public static final byte[] COMMAND_ENABLE_HR_SLEEP_MEASUREMENT = new byte[]{0x15, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_HR_SLEEP_MEASUREMENT = new byte[]{0x15, 0x00, 0x00};
public static final byte[] COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{0x06, 0x05, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{0x06, 0x05, 0x00, 0x00};
static {
MIBAND_DEBUG = new HashMap<>();
MIBAND_DEBUG.put(UUID_SERVICE_MIBAND_SERVICE, "MiBand Service");
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");
// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_NOTIFICATION, "Notification");
// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_USER_INFO, "User Info");
// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_CONTROL_POINT, "Control Point");
// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_REALTIME_STEPS, "Realtime Steps");
// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_ACTIVITY_DATA, "Activity Data");
// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_FIRMWARE_DATA, "Firmware Data");
// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_LE_PARAMS, "LE Params");
// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DATE_TIME, "Date/Time");
// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_STATISTICS, "Statistics");
// MIBAND_DEBUG.put(UUID_CHARACTERISTIC_BATTERY, "Battery");
// 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) {
String name = MIBAND_DEBUG.get(uuid);
if (name == null) {
name = fallback;
}
return name;
}
}

View File

@ -17,13 +17,13 @@ public final class MiBandConst {
public static final String PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR = "mi_reserve_alarm_calendar";
public static final String PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION = "mi_hr_sleep_detection";
public static final String PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS = "mi_device_time_offset_hours";
public static final String PREF_MI2_DATEFORMAT = "mi2_dateformat";
public static final String PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT = "mi2_activate_display_on_lift_wrist";
public static final String ORIGIN_SMS = "sms";
public static final String ORIGIN_INCOMING_CALL = "incoming_call";
public static final String ORIGIN_K9MAIL = "k9mail";
public static final String ORIGIN_PEBBLEMSG = "pebblemsg";
public static final String ORIGIN_GENERIC = "generic";
public static final String MI_GENERAL_NAME_PREFIX = "MI";
public static final String MI_BAND2_NAME = "MI Band 2";
public static final String MI_1 = "1";
public static final String MI_1A = "1A";
public static final String MI_1S = "1S";

View File

@ -1,18 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelUuid;
import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
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.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
@ -21,22 +36,51 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class MiBandCoordinator extends AbstractDeviceCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(MiBandCoordinator.class);
private final MiBandSampleProvider sampleProvider;
public MiBandCoordinator() {
sampleProvider = new MiBandSampleProvider();
}
@NonNull
@Override
public boolean supports(GBDeviceCandidate candidate) {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public Collection<? extends ScanFilter> createBLEScanFilters() {
ParcelUuid mi1Service = new ParcelUuid(MiBandService.UUID_SERVICE_MIBAND_SERVICE);
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(mi1Service).build();
return Collections.singletonList(filter);
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String macAddress = candidate.getMacAddress().toUpperCase();
return macAddress.startsWith(MiBandService.MAC_ADDRESS_FILTER_1_1A)
|| macAddress.startsWith(MiBandService.MAC_ADDRESS_FILTER_1S);
if (macAddress.startsWith(MiBandService.MAC_ADDRESS_FILTER_1_1A)
|| macAddress.startsWith(MiBandService.MAC_ADDRESS_FILTER_1S)) {
return DeviceType.MIBAND;
}
if (candidate.supportsService(MiBandService.UUID_SERVICE_MIBAND_SERVICE)
&& !candidate.supportsService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE)) {
return DeviceType.MIBAND;
}
// and a heuristic
try {
BluetoothDevice device = candidate.getDevice();
if (isHealthWearable(device)) {
String name = device.getName();
if (name != null && name.toUpperCase().startsWith(MiBandConst.MI_GENERAL_NAME_PREFIX.toUpperCase())) {
return DeviceType.MIBAND;
}
}
} catch (Exception ex) {
LOG.error("unable to check device support", ex);
}
return DeviceType.UNKNOWN;
}
@Override
public boolean supports(GBDevice device) {
return getDeviceType().equals(device.getType());
protected void deleteDevice(GBDevice gbDevice, Device device, DaoSession session) throws GBException {
Long deviceId = device.getId();
QueryBuilder<?> qb = session.getMiBandActivitySampleDao().queryBuilder();
qb.where(MiBandActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
}
@Override
@ -49,13 +93,14 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return MiBandPairingActivity.class;
}
@Override
public Class<? extends Activity> getPrimaryActivity() {
return ChartsActivity.class;
}
@Override
public SampleProvider getSampleProvider() {
return sampleProvider;
public SampleProvider<? extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new MiBandSampleProvider(device, session);
}
@Override
@ -79,11 +124,31 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return true;
}
@Override
public boolean supportsActivityTracking() {
return true;
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_activity;
}
@Override
public String getManufacturer() {
return "Xiaomi";
}
@Override
public boolean supportsAppsManagement() {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
public static boolean hasValidUserInfo() {
String dummyMacAddress = MiBandService.MAC_ADDRESS_FILTER_1_1A + ":00:00:00";
try {
@ -122,10 +187,10 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
UserInfo info = UserInfo.create(
miBandAddress,
prefs.getString(MiBandConst.PREF_USER_ALIAS, null),
activityUser.getActivityUserGender(),
activityUser.getActivityUserAge(),
activityUser.getActivityUserHeightCm(),
activityUser.getActivityUserWeightKg(),
activityUser.getGender(),
activityUser.getAge(),
activityUser.getHeightCm(),
activityUser.getWeightKg(),
0
);
return info;
@ -159,4 +224,18 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
Prefs prefs = GBApplication.getPrefs();
return prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0);
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
String hwVersion = device.getModel();
return isMi1S(hwVersion) || isMiPro(hwVersion);
}
private boolean isMi1S(String hardwareVersion) {
return MiBandConst.MI_1S.equals(hardwareVersion);
}
private boolean isMiPro(String hardwareVersion) {
return MiBandConst.MI_PRO.equals(hardwareVersion);
}
}

View File

@ -47,6 +47,9 @@ public class MiBandFWHelper {
16779782, //1.0.10.6 reported on the wiki
16779787, //1.0.10.11 tested by developer
//FW_16779790, //1.0.10.14 reported on the wiki (vibration does not work currently)
68094986, // 4.15.12.10 tested by developer
68158215, // 4.16.3.7 tested by developer
68158486, // 4.16.4.22 tested by developer and user
84870926, // 5.15.7.14 tested by developer
};

View File

@ -1,6 +1,5 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
@ -21,12 +20,13 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.GBActivity;
import nodomain.freeyourgadget.gadgetbridge.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 {
public class MiBandPairingActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(MiBandPairingActivity.class);
private static final int REQ_CODE_USER_SETTINGS = 52;
@ -43,8 +43,12 @@ public class MiBandPairingActivity extends Activity {
if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
LOG.debug("pairing activity: device changed: " + device);
if (macAddress.equals(device.getAddress()) && device.isInitialized()) {
pairingFinished(true);
if (macAddress.equals(device.getAddress())) {
if (device.isInitialized()) {
pairingFinished(true, macAddress);
} else if (device.isConnecting() || device.isInitializing()) {
LOG.info("still connecting/initializing device...");
}
}
}
}
@ -55,24 +59,38 @@ public class MiBandPairingActivity extends Activity {
public void onReceive(Context context, Intent intent) {
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
LOG.info("Bond state changed: " + device + ", state: " + device.getBondState() + ", expected address: " + bondingMacAddress);
if (bondingMacAddress != null && bondingMacAddress.equals(device.getAddress())) {
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
if (bondState == BluetoothDevice.BOND_BONDED) {
LOG.info("Bonded with " + device.getAddress());
bondingMacAddress = null;
Looper mainLooper = Looper.getMainLooper();
new Handler(mainLooper).postDelayed(new Runnable() {
@Override
public void run() {
performPair();
}
}, DELAY_AFTER_BONDING);
attemptToConnect();
} else if (bondState == BluetoothDevice.BOND_BONDING) {
LOG.info("Bonding in progress with " + device.getAddress());
} else if (bondState == BluetoothDevice.BOND_NONE) {
LOG.info("Not bonded with " + device.getAddress() + ", attempting to connect anyway.");
bondingMacAddress = null;
attemptToConnect();
} else {
LOG.warn("Unknown bond state for device " + device.getAddress() + ": " + bondState);
pairingFinished(false, bondingMacAddress);
}
}
}
}
};
private void attemptToConnect() {
Looper mainLooper = Looper.getMainLooper();
new Handler(mainLooper).postDelayed(new Runnable() {
@Override
public void run() {
performPair();
}
}, DELAY_AFTER_BONDING);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -142,7 +160,7 @@ public class MiBandPairingActivity extends Activity {
private void startPairing() {
isPairing = true;
message.setText(getString(R.string.miband_pairing, macAddress));
message.setText(getString(R.string.pairing, macAddress));
IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
@ -157,7 +175,7 @@ public class MiBandPairingActivity extends Activity {
}
}
private void pairingFinished(boolean pairedSuccessfully) {
private void pairingFinished(boolean pairedSuccessfully, String macAddress) {
LOG.debug("pairingFinished: " + pairedSuccessfully);
if (!isPairing) {
// already gone?
@ -169,12 +187,17 @@ public class MiBandPairingActivity extends Activity {
unregisterReceiver(mBondingReceiver);
if (pairedSuccessfully) {
Prefs prefs = GBApplication.getPrefs();
prefs.getPreferences().edit().putString(MiBandConst.PREF_MIBAND_ADDRESS, macAddress).apply();
// remember the device since we do not necessarily pair... temporary -- we probably need
// to query the db for available devices in ControlCenter. But only remember un-bonded
// devices, as bonded devices are displayed anyway.
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
if (device != null && device.getBondState() == BluetoothDevice.BOND_NONE) {
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);
startActivity(intent);
}
Intent intent = new Intent(this, ControlCenter.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
}
@ -186,24 +209,25 @@ public class MiBandPairingActivity extends Activity {
protected void performBluetoothPair(BluetoothDevice device) {
int bondState = device.getBondState();
if (bondState == BluetoothDevice.BOND_BONDED) {
LOG.info("Already bonded: " + device.getAddress());
GB.toast(getString(R.string.pairing_already_bonded, device.getName(), device.getAddress()), Toast.LENGTH_SHORT, GB.INFO);
performPair();
return;
}
bondingMacAddress = device.getAddress();
if (bondState == BluetoothDevice.BOND_BONDING) {
GB.toast(this, "Bonding in progress: " + bondingMacAddress, Toast.LENGTH_LONG, GB.INFO);
GB.toast(this, getString(R.string.pairing_in_progress, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.INFO);
return;
}
GB.toast(this, "Creating bond with" + bondingMacAddress, Toast.LENGTH_LONG, GB.INFO);
GB.toast(this, getString(R.string.pairing_creating_bond_with, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.INFO);
if (!device.createBond()) {
GB.toast(this, "Unable to pair with " + bondingMacAddress, Toast.LENGTH_LONG, GB.ERROR);
GB.toast(this, getString(R.string.pairing_unable_to_pair_with, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.ERROR);
}
}
private void performPair() {
GBApplication.deviceService().disconnect(); // just to make sure...
GBApplication.deviceService().connect(macAddress, true);
}
}

View File

@ -4,27 +4,29 @@ import android.content.Intent;
import android.os.Bundle;
import android.preference.Preference;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.Toast;
import java.util.HashSet;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_GENERIC;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_INCOMING_CALL;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_K9MAIL;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_PEBBLEMSG;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_SMS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DATEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ADDRESS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_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.VIBRATION_COUNT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PROFILE;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefKey;
public class MiBandPreferencesActivity extends AbstractSettingsActivity {
@ -34,17 +36,7 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
addPreferencesFromResource(R.xml.miband_preferences);
final Preference developmentMiaddr = findPreference(PREF_MIBAND_ADDRESS);
developmentMiaddr.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
preference.setSummary(newVal.toString());
return true;
}
});
addTryListeners();
final Preference enableHeartrateSleepSupport = findPreference(PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION);
enableHeartrateSleepSupport.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@ -54,21 +46,112 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
return true;
}
});
final Preference setDateFormat = findPreference(PREF_MI2_DATEFORMAT);
setDateFormat.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DATEFORMAT);
}
});
return true;
}
});
final Preference activateDisplayOnLift = findPreference(PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT);
activateDisplayOnLift.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT);
}
});
return true;
}
});
final Preference fitnessGoal = findPreference(PREF_MIBAND_FITNESS_GOAL);
fitnessGoal.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MIBAND_FITNESS_GOAL);
}
});
return true;
}
});
}
/**
* delayed execution so that the preferences are applied first
*/
private void invokeLater(Runnable runnable) {
getListView().post(runnable);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
final Preference developmentMiaddr = findPreference(PREF_MIBAND_ADDRESS);
developmentMiaddr.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
preference.setSummary(newVal.toString());
return true;
}
});
}
private void addTryListeners() {
for (final NotificationType type : NotificationType.values()) {
String prefKey = "mi_try_" + type.getGenericType();
final Preference tryPref = findPreference(prefKey);
if (tryPref != null) {
tryPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
tryVibration(type);
return true;
}
});
} else {
GB.toast(getBaseContext(), "Unable to find preference key: " + prefKey + ", trying the vibration won't work", Toast.LENGTH_LONG, GB.WARN);
}
}
}
private void tryVibration(NotificationType type) {
NotificationSpec spec = new NotificationSpec();
spec.type = type;
GBApplication.deviceService().onNotification(spec);
}
@Override
protected String[] getPreferenceKeysWithSummary() {
return new String[]{
PREF_USER_ALIAS,
PREF_MIBAND_ADDRESS,
PREF_MIBAND_FITNESS_GOAL,
PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR,
PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS,
getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_SMS),
getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_INCOMING_CALL),
getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_K9MAIL),
getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_PEBBLEMSG),
getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_GENERIC),
};
Set<String> prefKeys = new HashSet<>();
prefKeys.add(PREF_USER_ALIAS);
prefKeys.add(PREF_MIBAND_ADDRESS);
prefKeys.add(PREF_MIBAND_FITNESS_GOAL);
prefKeys.add(PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR);
prefKeys.add(PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS);
prefKeys.add(getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_INCOMING_CALL));
for (NotificationType type : NotificationType.values()) {
String key = type.getGenericType();
prefKeys.add(getNotificationPrefKey(VIBRATION_COUNT, key));
}
return prefKeys.toArray(new String[0]);
}
}

View File

@ -1,11 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class MiBandSampleProvider implements SampleProvider {
public static final int TYPE_DEEP_SLEEP = 5;
public static final int TYPE_LIGHT_SLEEP = 4;
public class MiBandSampleProvider extends AbstractMiBandSampleProvider {
public static final int TYPE_DEEP_SLEEP = 4;
public static final int TYPE_LIGHT_SLEEP = 5;
public static final int TYPE_ACTIVITY = -1;
public static final int TYPE_UNKNOWN = -1;
public static final int TYPE_NONWEAR = 3;
@ -19,8 +21,14 @@ public class MiBandSampleProvider implements SampleProvider {
// public static final byte TYPE_USER = 100;
// public static final byte TYPE_WALKING = 1;
// maybe this should be configurable 256 seems way off, though.
private final float movementDivisor = 180.0f; //256.0f;
public MiBandSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public int getID() {
return SampleProvider.PROVIDER_MIBAND;
}
@Override
public int normalizeType(int rawType) {
@ -57,14 +65,4 @@ public class MiBandSampleProvider implements SampleProvider {
return TYPE_UNKNOWN;
}
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / movementDivisor;
}
@Override
public int getID() {
return SampleProvider.PROVIDER_MIBAND;
}
}

View File

@ -13,8 +13,9 @@ public class MiBandService {
public static final String MAC_ADDRESS_FILTER_1S = "C8:0F:10";
public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0"));
public static final UUID UUID_SERVICE_MIBAND2_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE1"));
public static final UUID UUID_SERVICE_HEART_RATE = UUID.fromString(String.format(BASE_UUID, "180D"));
public static final String UUID_SERVICE_WEIGHT_SERVICE = "00001530-0000-3512-2118-0009af100700";
public static final UUID UUID_CHARACTERISTIC_DEVICE_INFO = UUID.fromString(String.format(BASE_UUID, "FF01"));
@ -53,8 +54,6 @@ public class MiBandService {
/* 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";
public static final byte ALIAS_LEN = 0xa;
/*NOTIFICATIONS: usually received on the UUID_CHARACTERISTIC_NOTIFICATION characteristic */
@ -165,6 +164,7 @@ public class MiBandService {
public static final byte COMMAND_SET__HR_CONTINUOUS = 0x1;
public static final byte COMMAND_SET_HR_MANUAL = 0x2;
public static final byte COMMAND_GET_SENSOR_DATA = 0x12;
/* FURTHER COMMANDS: unchecked therefore left commented
@ -173,8 +173,6 @@ public class MiBandService {
public static final int COMMAND_SET_COLOR_THEME = et;
public static final byte COMMAND_GET_SENSOR_DATA = 0x12t
*/
/* CONNECTION: unknown
@ -209,17 +207,15 @@ public class MiBandService {
*/
/* TEST: unkown (maybe sent to UUID_CHARACTERISTIC_TEST characteristic?
// TEST_*: sent to UUID_CHARACTERISTIC_TEST characteristic
public static final TEST_DISCONNECTED_REMINDER = 0x5t
public static final byte TEST_DISCONNECTED_REMINDER = 0x5;
public static final TEST_NOTIFICATION = 0x3t
public static final byte TEST_NOTIFICATION = 0x3;
public static final TEST_REMOTE_DISCONNECT = 0x1t
public static final byte TEST_REMOTE_DISCONNECT = 0x1;
public static final TEST_SELFTEST = 0x2t
*/
public static final byte TEST_SELFTEST = 0x2;
private static final Map<UUID, String> MIBAND_DEBUG;

View File

@ -4,6 +4,7 @@ import android.content.Context;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertLevel;
public class VibrationProfile {
public static final Context CONTEXT = GBApplication.getContext();
@ -42,13 +43,14 @@ public class VibrationProfile {
private final int[] onOffSequence;
private final short repeat;
private int alertLevel = AlertLevel.MildAlert.getId();
/**
* Creates a new profile instance.
*
* @param id the ID, used as preference key.
* @param onOffSequence a sequence of alternating on and off durations, in milliseconds
* @param repeat how ofoften the sequence shall be repeated
* @param repeat how often the sequence shall be repeated
*/
public VibrationProfile(String id, int[] onOffSequence, short repeat) {
this.id = id;
@ -67,4 +69,12 @@ public class VibrationProfile {
public short getRepeat() {
return repeat;
}
public void setAlertLevel(int alertLevel) {
this.alertLevel = alertLevel;
}
public int getAlertLevel() {
return alertLevel;
}
}

View File

@ -1,52 +0,0 @@
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

@ -1,30 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
public class MisfitSampleProvider implements SampleProvider {
protected final float movementDivisor = 300f;
@Override
public int normalizeType(int rawType) {
return (int) rawType;
}
@Override
public int toRawActivityKind(int activityKind) {
return (byte) activityKind;
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / movementDivisor;
}
@Override
public int getID() {
return SampleProvider.PROVIDER_PEBBLE_MISFIT;
}
}

View File

@ -1,54 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class MorpheuzSampleProvider implements SampleProvider {
// raw types
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(int rawType) {
switch (rawType) {
case TYPE_DEEP_SLEEP:
return ActivityKind.TYPE_DEEP_SLEEP;
case TYPE_LIGHT_SLEEP:
return ActivityKind.TYPE_LIGHT_SLEEP;
case TYPE_ACTIVITY:
return ActivityKind.TYPE_ACTIVITY;
default:
// case TYPE_UNKNOWN: // fall through
return ActivityKind.TYPE_UNKNOWN;
}
}
@Override
public int toRawActivityKind(int activityKind) {
switch (activityKind) {
case ActivityKind.TYPE_ACTIVITY:
return TYPE_ACTIVITY;
case ActivityKind.TYPE_DEEP_SLEEP:
return TYPE_DEEP_SLEEP;
case ActivityKind.TYPE_LIGHT_SLEEP:
return TYPE_LIGHT_SLEEP;
case ActivityKind.TYPE_UNKNOWN: // fall through
default:
return TYPE_UNKNOWN;
}
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / movementDivisor;
}
@Override
public int getID() {
return SampleProvider.PROVIDER_PEBBLE_MORPHEUZ;
}
}

View File

@ -18,6 +18,7 @@ import java.io.Writer;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
@ -51,7 +52,7 @@ public class PBWInstallHandler implements InstallHandler {
return;
}
String platformName = PebbleUtils.getPlatformName(device.getHardwareVersion());
String platformName = PebbleUtils.getPlatformName(device.getModel());
try {
mPBWReader = new PBWReader(mUri, mContext, platformName);
@ -74,7 +75,7 @@ public class PBWInstallHandler implements InstallHandler {
installItem.setIcon(R.drawable.ic_firmware);
String hwRevision = mPBWReader.getHWRevision();
if (hwRevision != null && hwRevision.equals(device.getHardwareVersion())) {
if (hwRevision != null && hwRevision.equals(device.getModel())) {
installItem.setName(mContext.getString(R.string.pbw_installhandler_pebble_firmware, ""));
installItem.setDetails(mContext.getString(R.string.pbwinstallhandler_correct_hw_revision));
@ -135,6 +136,8 @@ public class PBWInstallHandler implements InstallHandler {
destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
destDir.mkdirs();
FileUtils.copyURItoFile(mContext, mUri, new File(destDir, app.getUUID().toString() + ".pbw"));
AppManagerActivity.addToAppOrderFile("pbwcacheorder.txt", app.getUUID());
} catch (IOException e) {
LOG.error("Installation failed: " + e.getMessage(), e);
return;
@ -174,11 +177,12 @@ public class PBWInstallHandler implements InstallHandler {
LOG.error("Failed to open output file: " + e.getMessage(), e);
}
}
}
public boolean isValid() {
// always pretend it is valid, as we cant know yet about hw/fw version
// always pretend it is valid, as we can't know yet about hw/fw version
return true;
}
}
}

View File

@ -96,23 +96,38 @@ public class PBWReader {
String platformDir = "";
if (!uri.toString().endsWith(".pbz")) {
platformDir = platform + "/";
/*
* for aplite and basalt it is possible to install 2.x apps which have no subfolder
* we still prefer the subfolders if present.
* chalk needs to be its subfolder
*/
if (platform.equals("aplite") || platform.equals("basalt")) {
boolean hasPlatformDir = false;
String[] platformDirs;
switch (platform) {
case "basalt":
platformDirs = new String[]{"basalt/"};
break;
case "chalk":
platformDirs = new String[]{"chalk/"};
break;
case "diorite":
platformDirs = new String[]{"diorite/", "aplite/"};
break;
case "emery":
platformDirs = new String[]{"emery/", "basalt/"};
break;
default:
platformDirs = new String[]{"aplite/"};
}
for (String dir : platformDirs) {
InputStream afin = new BufferedInputStream(cr.openInputStream(uri));
ZipInputStream zis = new ZipInputStream(afin);
ZipEntry ze;
try {
while ((ze = zis.getNextEntry()) != null) {
if (ze.getName().startsWith(platformDir)) {
hasPlatformDir = true;
if (ze.getName().startsWith(dir)) {
platformDir = dir;
break;
}
}
@ -120,13 +135,13 @@ public class PBWReader {
} catch (IOException e) {
e.printStackTrace();
}
}
if (!hasPlatformDir) {
platformDir = "";
}
if (platform.equals("chalk") && platformDir.equals("")) {
return;
}
}
LOG.info("using platformdir: '" + platformDir + "'");
String appName = null;
String appCreator = null;
String appVersion = null;
@ -217,7 +232,7 @@ public class PBWReader {
byte[] tmp_buf = new byte[32];
ByteBuffer buf = ByteBuffer.wrap(buffer);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.getLong(); // header, TODO: verifiy
buf.getLong(); // header, TODO: verify
buf.getShort(); // struct version, TODO: verify
mSdkVersion = buf.getShort();
mAppVersion = buf.getShort();
@ -327,4 +342,4 @@ public class PBWReader {
public JSONObject getAppKeysJSON() {
return mAppKeys;
}
}
}

View File

@ -4,15 +4,25 @@ import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlayDao;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMisfitSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class PebbleCoordinator extends AbstractDeviceCoordinator {
@ -20,13 +30,12 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
}
@Override
public boolean supports(GBDeviceCandidate candidate) {
return candidate.getName().startsWith("Pebble");
}
@Override
public boolean supports(GBDevice device) {
return getDeviceType().equals(device.getType());
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
if (name != null && name.startsWith("Pebble")) {
return DeviceType.PEBBLE;
}
return DeviceType.UNKNOWN;
}
@Override
@ -36,28 +45,40 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
return PebblePairingActivity.class;
}
@Override
public Class<? extends Activity> getPrimaryActivity() {
return AppManagerActivity.class;
}
@Override
public SampleProvider getSampleProvider() {
protected void deleteDevice(GBDevice gbDevice, Device device, DaoSession session) throws GBException {
Long deviceId = device.getId();
QueryBuilder<?> qb = session.getPebbleHealthActivitySampleDao().queryBuilder();
qb.where(PebbleHealthActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getPebbleHealthActivityOverlayDao().queryBuilder();
qb.where(PebbleHealthActivityOverlayDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getPebbleMisfitSampleDao().queryBuilder();
qb.where(PebbleMisfitSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getPebbleMorpheuzSampleDao().queryBuilder();
qb.where(PebbleMorpheuzSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
}
@Override
public SampleProvider<? extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
Prefs prefs = GBApplication.getPrefs();
int activityTracker = prefs.getInt("pebble_activitytracker", SampleProvider.PROVIDER_PEBBLE_HEALTH);
switch (activityTracker) {
case SampleProvider.PROVIDER_PEBBLE_HEALTH:
return new HealthSampleProvider();
return new PebbleHealthSampleProvider(device, session);
case SampleProvider.PROVIDER_PEBBLE_MISFIT:
return new MisfitSampleProvider();
return new PebbleMisfitSampleProvider(device, session);
case SampleProvider.PROVIDER_PEBBLE_MORPHEUZ:
return new MorpheuzSampleProvider();
case SampleProvider.PROVIDER_PEBBLE_GADGETBRIDGE:
return new PebbleGadgetBridgeSampleProvider();
return new PebbleMorpheuzSampleProvider(device, session);
default:
return new HealthSampleProvider();
return new PebbleHealthSampleProvider(device, session);
}
}
@ -72,6 +93,11 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsActivityTracking() {
return true;
}
@Override
public boolean supportsScreenshots() {
return true;
@ -82,8 +108,28 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return PebbleUtils.hasHRM(device.getModel());
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_app_mananger;
}
@Override
public String getManufacturer() {
return "Pebble";
}
@Override
public boolean supportsAppsManagement() {
return true;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return AppManagerActivity.class;
}
}

View File

@ -1,14 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
public class PebbleGadgetBridgeSampleProvider extends MorpheuzSampleProvider {
public PebbleGadgetBridgeSampleProvider() {
movementDivisor = 63.0f;
}
@Override
public int getID() {
return SampleProvider.PROVIDER_PEBBLE_GADGETBRIDGE;
}
}

View File

@ -0,0 +1,130 @@
package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlay;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlayDao;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class PebbleHealthSampleProvider extends AbstractSampleProvider<PebbleHealthActivitySample> {
public static final int TYPE_LIGHT_SLEEP = 1;
public static final int TYPE_DEEP_SLEEP = 2;
public static final int TYPE_LIGHT_NAP = 3; //probably
public static final int TYPE_DEEP_NAP = 4; //probably
public static final int TYPE_WALK = 5; //probably
public static final int TYPE_ACTIVITY = -1;
protected final float movementDivisor = 8000f;
public PebbleHealthSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public List<PebbleHealthActivitySample> getAllActivitySamples(int timestamp_from, int timestamp_to) {
List<PebbleHealthActivitySample> samples = super.getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL);
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
// no device, no samples
return Collections.emptyList();
}
QueryBuilder<PebbleHealthActivityOverlay> qb = getSession().getPebbleHealthActivityOverlayDao().queryBuilder();
// I assume it returns the records by id ascending ... (last overlay is dominant)
qb.where(PebbleHealthActivityOverlayDao.Properties.DeviceId.eq(dbDevice.getId()), PebbleHealthActivityOverlayDao.Properties.TimestampFrom.ge(timestamp_from))
.where(PebbleHealthActivityOverlayDao.Properties.TimestampTo.le(timestamp_to));
List<PebbleHealthActivityOverlay> overlayRecords = qb.build().list();
for (PebbleHealthActivityOverlay overlay : overlayRecords) {
for (PebbleHealthActivitySample sample : samples) {
if (overlay.getTimestampFrom() <= sample.getTimestamp() && sample.getTimestamp() < overlay.getTimestampTo()) {
// patch in the raw kind
sample.setRawKind(overlay.getRawKind());
}
}
}
detachFromSession();
return samples;
}
@Override
public AbstractDao<PebbleHealthActivitySample, ?> getSampleDao() {
return getSession().getPebbleHealthActivitySampleDao();
}
@Override
protected Property getTimestampSampleProperty() {
return PebbleHealthActivitySampleDao.Properties.Timestamp;
}
@Override
protected Property getRawKindSampleProperty() {
return null;
// it is still in the database just hide it for now. remove these two commented lines later
//return PebbleHealthActivitySampleDao.Properties.RawKind;
}
@Override
protected Property getDeviceIdentifierSampleProperty() {
return PebbleHealthActivitySampleDao.Properties.DeviceId;
}
@Override
public PebbleHealthActivitySample createActivitySample() {
return new PebbleHealthActivitySample();
}
@Override
public int normalizeType(int rawType) {
switch (rawType) {
case TYPE_DEEP_NAP:
case TYPE_DEEP_SLEEP:
return ActivityKind.TYPE_DEEP_SLEEP;
case TYPE_LIGHT_NAP:
case TYPE_LIGHT_SLEEP:
return ActivityKind.TYPE_LIGHT_SLEEP;
case TYPE_ACTIVITY:
return ActivityKind.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;
default:
return TYPE_ACTIVITY;
}
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / movementDivisor;
}
@Override
public int getID() {
return SampleProvider.PROVIDER_PEBBLE_HEALTH;
}
}

View File

@ -85,11 +85,25 @@ public final class PebbleIconID {
public static final int SETTINGS = 83;
public static final int SUNRISE = 84;
public static final int SUNSET = 85;
public static final int FACETIME_DISMISSED = 86;
public static final int FACETIME_INCOMING = 87;
public static final int FACETIME_OUTGOING = 88;
public static final int FACETIME_MISSED = 89;
public static final int FACETIME_DURING = 90;
public static final int BLUESCREEN_OF_DEATH = 91;
public static final int START_MUSIC_PHONE = 92;
public static final int RESULT_UNMUTE = 86;
public static final int RESULT_UNMUTE_ALT = 94;
public static final int DURING_PHONE_CALL_CENTERED = 95;
public static final int TIMELINE_EMPTY_CALENDAR = 96;
public static final int THUMBS_UP = 97;
public static final int ARROW_UP = 98;
public static final int ARROW_DOWN = 99;
public static final int ACTIVITY = 100;
public static final int SLEEP = 101;
public static final int REWARD_BAD = 102;
public static final int REWARD_GOOD = 103;
public static final int REWARD_AVERAGE = 104;
public static final int NOTIFICATION_FACETIME = 110;
// 4.x only from here
public static final int NOTIFICATION_AMAZON = 111;
public static final int NOTIFICATION_GOOGLE_MAPS = 112;
public static final int NOTIFICATION_GOOGLE_PHOTOS = 113;
public static final int NOTIFICATION_IOS_PHOTOS = 114;
public static final int NOTIFICATION_LINKEDIN = 115;
public static final int NOTIFICATION_SLACK = 116;
}

View File

@ -0,0 +1,62 @@
package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMisfitSample;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMisfitSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class PebbleMisfitSampleProvider extends AbstractSampleProvider<PebbleMisfitSample> {
protected final float movementDivisor = 300f;
public PebbleMisfitSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public int normalizeType(int rawType) {
return rawType;
}
@Override
public int toRawActivityKind(int activityKind) {
return activityKind;
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / movementDivisor;
}
@Override
public PebbleMisfitSample createActivitySample() {
return new PebbleMisfitSample();
}
@Override
public int getID() {
return SampleProvider.PROVIDER_PEBBLE_MISFIT;
}
@Override
public AbstractDao<PebbleMisfitSample, ?> getSampleDao() {
return getSession().getPebbleMisfitSampleDao();
}
@Override
protected Property getRawKindSampleProperty() {
return null;
}
protected Property getTimestampSampleProperty() {
return PebbleMisfitSampleDao.Properties.Timestamp;
}
protected Property getDeviceIdentifierSampleProperty() {
return PebbleMisfitSampleDao.Properties.DeviceId;
}
}

View File

@ -0,0 +1,65 @@
package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSample;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class PebbleMorpheuzSampleProvider extends AbstractSampleProvider<PebbleMorpheuzSample> {
protected float movementDivisor = 5000f;
public PebbleMorpheuzSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public AbstractDao<PebbleMorpheuzSample, ?> getSampleDao() {
return getSession().getPebbleMorpheuzSampleDao();
}
@Override
protected Property getTimestampSampleProperty() {
return PebbleMorpheuzSampleDao.Properties.Timestamp;
}
@Override
protected Property getRawKindSampleProperty() {
return null; // not supported
}
@Override
protected Property getDeviceIdentifierSampleProperty() {
return PebbleMorpheuzSampleDao.Properties.DeviceId;
}
@Override
public PebbleMorpheuzSample createActivitySample() {
return new PebbleMorpheuzSample();
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity / movementDivisor;
}
@Override
public int getID() {
return SampleProvider.PROVIDER_PEBBLE_MORPHEUZ;
}
@Override
public int normalizeType(int rawType) {
return rawType;
}
@Override
public int toRawActivityKind(int activityKind) {
return activityKind;
}
}

View File

@ -0,0 +1,242 @@
package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.TextView;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import de.greenrobot.dao.query.Query;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.GBActivity;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class PebblePairingActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(PebblePairingActivity.class);
private TextView message;
private boolean isPairing;
private boolean isLEPebble;
private String macAddress;
private BluetoothDevice mBtDevice;
private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
LOG.debug("pairing activity: device changed: " + device);
if (macAddress.equals(device.getAddress()) || macAddress.equals(device.getVolatileAddress())) {
if (device.isInitialized()) {
pairingFinished(true);
} else if (device.isConnecting() || device.isInitializing()) {
LOG.info("still connecting/initializing device...");
}
}
}
}
};
private final BroadcastReceiver mBondingReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
LOG.info("Bond state changed: " + device + ", state: " + device.getBondState() + ", expected address: " + macAddress);
if (macAddress != null && macAddress.equals(device.getAddress())) {
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
if (bondState == BluetoothDevice.BOND_BONDED) {
LOG.info("Bonded with " + device.getAddress());
if (!isLEPebble) {
performConnect(null);
}
} else if (bondState == BluetoothDevice.BOND_BONDING) {
LOG.info("Bonding in progress with " + device.getAddress());
} else if (bondState == BluetoothDevice.BOND_NONE) {
LOG.info("Not bonded with " + device.getAddress() + ", attempting to connect anyway.");
} else {
LOG.warn("Unknown bond state for device " + device.getAddress() + ": " + bondState);
pairingFinished(false);
}
}
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pebble_pairing);
message = (TextView) findViewById(R.id.pebble_pair_message);
Intent intent = getIntent();
macAddress = intent.getStringExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS);
if (macAddress == null) {
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
returnToPairingActivity();
return;
}
mBtDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
if (mBtDevice == null) {
GB.toast(this, "No such Bluetooth Device: " + macAddress, Toast.LENGTH_LONG, GB.ERROR);
returnToPairingActivity();
return;
}
isLEPebble = mBtDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE;
GBDevice gbDevice = null;
if (isLEPebble) {
if (mBtDevice.getName().startsWith("Pebble-LE ") || mBtDevice.getName().startsWith("Pebble Time LE ")) {
if (!GBApplication.getPrefs().getBoolean("pebble_force_le", false)) {
GB.toast(this, "Please switch on \"Always prefer BLE\" option in Pebble settings before pairing you Pebble LE", Toast.LENGTH_LONG, GB.ERROR);
returnToPairingActivity();
return;
}
gbDevice = getMatchingParentDeviceFromDB(mBtDevice);
if (gbDevice == null) {
return;
}
}
}
startPairing(gbDevice);
}
@Override
protected void onDestroy() {
try {
// just to be sure, remove the receivers -- might actually be already unregistered
LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
unregisterReceiver(mBondingReceiver);
} catch (IllegalArgumentException ex) {
// already unregistered, ignore
}
if (isPairing) {
stopPairing();
}
super.onDestroy();
}
private void startPairing(GBDevice gbDevice) {
isPairing = true;
message.setText(getString(R.string.pairing, macAddress));
IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
registerReceiver(mBondingReceiver, filter);
performPair(gbDevice);
}
private void pairingFinished(boolean pairedSuccessfully) {
LOG.debug("pairingFinished: " + pairedSuccessfully);
if (!isPairing) {
// already gone?
return;
}
isPairing = false;
LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
unregisterReceiver(mBondingReceiver);
if (pairedSuccessfully) {
Intent intent = new Intent(this, ControlCenter.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
finish();
}
private void stopPairing() {
// TODO
isPairing = false;
}
protected void performPair(GBDevice gbDevice) {
int bondState = mBtDevice.getBondState();
if (bondState == BluetoothDevice.BOND_BONDED) {
GB.toast(getString(R.string.pairing_already_bonded, mBtDevice.getName(), mBtDevice.getAddress()), Toast.LENGTH_SHORT, GB.INFO);
return;
}
if (bondState == BluetoothDevice.BOND_BONDING) {
GB.toast(this, getString(R.string.pairing_in_progress, mBtDevice.getName(), macAddress), Toast.LENGTH_LONG, GB.INFO);
return;
}
GB.toast(this, getString(R.string.pairing_creating_bond_with, mBtDevice.getName(), macAddress), Toast.LENGTH_LONG, GB.INFO);
GBApplication.deviceService().disconnect(); // just to make sure...
if (isLEPebble) {
performConnect(gbDevice);
} else {
mBtDevice.createBond();
}
}
private void performConnect(GBDevice gbDevice) {
if (gbDevice == null) {
gbDevice = new GBDevice(mBtDevice.getAddress(), mBtDevice.getName(), DeviceType.PEBBLE);
}
GBApplication.deviceService().connect(gbDevice);
}
private void returnToPairingActivity() {
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
finish();
}
private GBDevice getMatchingParentDeviceFromDB(BluetoothDevice btDevice) {
String expectedSuffix = btDevice.getName();
expectedSuffix = expectedSuffix.replace("Pebble-LE ", "");
expectedSuffix = expectedSuffix.replace("Pebble Time LE ", "");
expectedSuffix = expectedSuffix.substring(0, 2) + ":" + expectedSuffix.substring(2);
LOG.info("will try to find a Pebble with BT address suffix " + expectedSuffix);
GBDevice gbDevice = null;
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
DeviceDao deviceDao = session.getDeviceDao();
Query<Device> query = deviceDao.queryBuilder().where(DeviceDao.Properties.Type.eq(1), DeviceDao.Properties.Identifier.like("%" + expectedSuffix)).build();
List<Device> devices = query.list();
if (devices.size() == 0) {
GB.toast("Please pair your non-LE Pebble before pairing the LE one", Toast.LENGTH_SHORT, GB.INFO);
returnToPairingActivity();
return null;
} else if (devices.size() > 1) {
GB.toast("Can not match this Pebble LE to a unique device", Toast.LENGTH_SHORT, GB.INFO);
returnToPairingActivity();
return null;
}
DeviceHelper deviceHelper = DeviceHelper.getInstance();
gbDevice = deviceHelper.toGBDevice(devices.get(0));
gbDevice.setVolatileAddress(btDevice.getAddress());
} catch (Exception e) {
GB.toast("Error retrieving devices from database", Toast.LENGTH_SHORT, GB.ERROR);
returnToPairingActivity();
return null;
}
return gbDevice;
}
}

View File

@ -0,0 +1,105 @@
package nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.VibrationActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
if (name != null && name.startsWith("Vibratissimo")) {
return DeviceType.VIBRATISSIMO;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.VIBRATISSIMO;
}
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public Class<? extends Activity> getPrimaryActivity() {
return VibrationActivity.class;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsActivityDataFetching() {
return false;
}
@Override
public boolean supportsActivityTracking() {
return false;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
}
@Override
public boolean supportsScreenshots() {
return false;
}
@Override
public boolean supportsAlarmConfiguration() {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_vibration;
}
@Override
public String getManufacturer() {
return "Amor AG";
}
@Override
public boolean supportsAppsManagement() {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
// nothing to delete, yet
}
}

View File

@ -0,0 +1 @@
*.java

View File

@ -0,0 +1,89 @@
package nodomain.freeyourgadget.gadgetbridge.entities;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public abstract class AbstractActivitySample implements ActivitySample {
private SampleProvider mProvider;
@Override
public SampleProvider getProvider() {
return mProvider;
}
public void setProvider(SampleProvider provider) {
mProvider = provider;
}
@Override
public int getKind() {
return getProvider().normalizeType(getRawKind());
}
@Override
public int getRawKind() {
return NOT_MEASURED;
}
@Override
public float getIntensity() {
return getProvider().normalizeIntensity(getRawIntensity());
}
public void setRawKind(int kind) {
}
public void setRawIntensity(int intensity) {
}
public void setSteps(int steps) {
}
/**
* Unix timestamp of the sample, i.e. the number of seconds since 1970-01-01 00:00:00 UTC.
*/
public abstract void setTimestamp(int timestamp);
public abstract void setUserId(long userId);
@Override
public void setHeartRate(int heartRate) {
}
@Override
public int getHeartRate() {
return NOT_MEASURED;
}
public abstract void setDeviceId(long deviceId);
public abstract long getDeviceId();
public abstract long getUserId();
@Override
public int getRawIntensity() {
return NOT_MEASURED;
}
@Override
public int getSteps() {
return NOT_MEASURED;
}
@Override
public String toString() {
int kind = getProvider() != null ? getKind() : ActivitySample.NOT_MEASURED;
float intensity = getProvider() != null ? getIntensity() : ActivitySample.NOT_MEASURED;
return getClass().getSimpleName() + "{" +
"timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimeStamp(getTimestamp())) +
", intensity=" + intensity +
", steps=" + getSteps() +
", heartrate=" + getHeartRate() +
", type=" + kind +
", userId=" + getUserId() +
", deviceId=" + getDeviceId() +
'}';
}
}

View File

@ -0,0 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.entities;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public abstract class AbstractPebbleHealthActivitySample extends AbstractActivitySample {
abstract public byte[] getRawPebbleHealthData();
private transient int rawActivityKind = ActivityKind.TYPE_UNKNOWN;
@Override
public int getRawKind() {
return rawActivityKind;
}
@Override
public void setRawKind(int kind) {
this.rawActivityKind = kind;
}
}

View File

@ -0,0 +1,54 @@
package nodomain.freeyourgadget.gadgetbridge.entities;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public abstract class AbstractPebbleMisfitActivitySample extends AbstractActivitySample {
abstract public int getRawPebbleMisfitSample();
private transient int intensity = 0;
private transient int steps = 0;
private transient int activityKind = ActivityKind.TYPE_UNKNOWN;
private void calculate() {
int sample = getRawPebbleMisfitSample();
if (((sample & 0x83ff) == 0x0001) && ((sample & 0xff00) <= 0x4800)) {
// sleep seems to be from 0x2401 to 0x4801 (0b0IIIII0000000001) where I = intensity ?
intensity = (sample & 0x7c00) >>> 10;
// 9-18 decimal after shift
if (intensity <= 13) {
activityKind = ActivityKind.TYPE_DEEP_SLEEP;
} else {
// FIXME: this leads to too much false positives, ignore for now
//activityKind = ActivityKind.TYPE_LIGHT_SLEEP;
//intensity *= 2; // better visual distinction
}
} else {
if ((sample & 0x0001) == 0) { // 16-??? steps encoded in bits 1-7
steps = (sample & 0x00fe);
} else { // 0-14 steps encoded in bits 1-3, most of the time fc71 bits are set in that case
steps = (sample & 0x000e);
}
intensity = steps;
activityKind = ActivityKind.TYPE_ACTIVITY;
}
}
@Override
public int getSteps() {
calculate();
return steps;
}
@Override
public int getKind() {
calculate();
return activityKind;
}
@Override
public int getRawIntensity() {
calculate();
return intensity;
}
}

View File

@ -0,0 +1,17 @@
package nodomain.freeyourgadget.gadgetbridge.entities;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public abstract class AbstractPebbleMorpheuzActivitySample extends AbstractActivitySample {
@Override
public int getKind() {
int rawIntensity = getRawIntensity();
if (rawIntensity <= 120) {
return ActivityKind.TYPE_DEEP_SLEEP;
} else if (rawIntensity <= 1000) {
return ActivityKind.TYPE_LIGHT_SLEEP;
}
return ActivityKind.TYPE_ACTIVITY;
}
}

View File

@ -1,11 +1,17 @@
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.Manifest;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationManager;
import android.support.v4.app.ActivityCompat;
import net.e175.klaus.solarpositioning.DeltaT;
import net.e175.klaus.solarpositioning.SPA;
@ -64,6 +70,21 @@ public class AlarmReceiver extends BroadcastReceiver {
float latitude = prefs.getFloat("location_latitude", 0);
float longitude = prefs.getFloat("location_longitude", 0);
LOG.info("got longitude/latitude from preferences: " + latitude + "/" + longitude);
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
prefs.getBoolean("use_updated_location_if_available", false)) {
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
String provider = locationManager.getBestProvider(criteria, false);
if (provider != null) {
Location lastKnownLocation = locationManager.getLastKnownLocation(provider);
if (lastKnownLocation != null) {
latitude = (float) lastKnownLocation.getLatitude();
longitude = (float) lastKnownLocation.getLongitude();
LOG.info("got longitude/latitude from last known location: " + latitude + "/" + longitude);
}
}
}
GregorianCalendar[] sunriseTransitSetTomorrow = SPA.calculateSunriseTransitSet(dateTimeTomorrow, latitude, longitude, DeltaT.estimate(dateTimeTomorrow));
CalendarEventSpec calendarEventSpec = new CalendarEventSpec();

View File

@ -7,7 +7,7 @@ import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class BluetoothStateChangeReceiver extends BroadcastReceiver {
@ -18,7 +18,7 @@ public class BluetoothStateChangeReceiver extends BroadcastReceiver {
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_ON) {
Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent);
Prefs prefs = GBApplication.getPrefs();
@ -28,11 +28,7 @@ public class BluetoothStateChangeReceiver extends BroadcastReceiver {
GBApplication.deviceService().connect();
} else if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_OFF) {
GBApplication.deviceService().quit();
Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
GBApplication.quit();
}
}
}

View File

@ -54,10 +54,10 @@ public class K9Receiver extends BroadcastReceiver {
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = -1;
notificationSpec.type = NotificationType.EMAIL;
notificationSpec.type = NotificationType.GENERIC_EMAIL;
/*
* there seems to be no way to specify the the uri in the where clause.
* there seems to be no way to specify the uri in the where clause.
* If we do so, we just get the newest message, not the one requested.
* So, we will just search our message and match the uri manually.
* It should be the first one returned by the query in most cases,

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