diff --git a/.travis.yml b/.travis.yml index d58ee5f8..522f9194 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c34cdbdc..75796e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/GBDaoGenerator/.gitignore b/GBDaoGenerator/.gitignore new file mode 100644 index 00000000..81631c69 --- /dev/null +++ b/GBDaoGenerator/.gitignore @@ -0,0 +1,2 @@ +/bin +/build diff --git a/GBDaoGenerator/build.gradle b/GBDaoGenerator/build.gradle new file mode 100644 index 00000000..756314f8 --- /dev/null +++ b/GBDaoGenerator/build.gradle @@ -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 +} diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java new file mode 100644 index 00000000..63e5d713 --- /dev/null +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -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; + } +} diff --git a/Get_it_on_F-Droid.svg.png b/Get_it_on_F-Droid.svg.png index cc547d02..75d6992e 100644 Binary files a/Get_it_on_F-Droid.svg.png and b/Get_it_on_F-Droid.svg.png differ diff --git a/README.md b/README.md index 6f888ce8..2c631edc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Gadgetbridge ============ -Gadgetbridge is an Android (4.4+) Application which will allow you to use your +Gadgetbridge is an Android (4.4+) application which will allow you to use your Pebble or Mi Band without the vendor's closed source application and without the need to create an account and transmit any of your data to the vendor's servers. @@ -15,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. diff --git a/app/build.gradle b/app/build.gradle index f6cd378f..573105c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 46562de5..9081b5a4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,7 +55,7 @@ android:parentActivityName=".activities.SettingsActivity" /> - + @@ -182,6 +182,15 @@ + + + + + + + + + + + + + + + android:parentActivityName=".activities.appmanager.AppManagerActivity"> diff --git a/app/src/main/assets/app_config/configure.html b/app/src/main/assets/app_config/configure.html index 14e4d7fd..6da22151 100644 --- a/app/src/main/assets/app_config/configure.html +++ b/app/src/main/assets/app_config/configure.html @@ -1,6 +1,6 @@ - + @@ -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; + } - -
-

Url of the configuration:

+ +
+

URL of the configuration:

- + +

App presets:

+
-
+
+

In case of "network error" after saving settings in the watchapp, copy the "network error" + URL and paste it here:

+
+ +
+

Incoming configuration data:

- + +

App Presets:

+ +

Existing presets will be deleted.

diff --git a/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js b/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js index df85886d..caf39669 100644 --- a/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js +++ b/app/src/main/assets/app_config/js/gadgetbridge_boilerplate.js @@ -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); \ No newline at end of file diff --git a/app/src/main/assets/devintro.png b/app/src/main/assets/devintro.png index 6fc1a593..859cfdee 100644 Binary files a/app/src/main/assets/devintro.png and b/app/src/main/assets/devintro.png differ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 0113effb..b0659383 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -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 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! + *

+ * 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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBEnvironment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBEnvironment.java index df07dc46..e30e7c5a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBEnvironment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBEnvironment.java @@ -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() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/LockHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/LockHandler.java new file mode 100644 index 00000000..979b3493 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/LockHandler.java @@ -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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java index 7ea3c8e9..3d71b91f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java @@ -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)); + } + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java index e501de49..1c8ed55a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SleepAlarmWidget.java @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java index 965a0cfd..8d9ebc10 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractSettingsActivity.java @@ -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; - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppCompatPreferenceActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppCompatPreferenceActivity.java new file mode 100644 index 00000000..40f21e34 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppCompatPreferenceActivity.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java deleted file mode 100644 index f66bd731..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppManagerActivity.java +++ /dev/null @@ -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 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 appList = new ArrayList<>(); - private GBDeviceAppAdapter mGBDeviceAppAdapter; - private GBDeviceApp selectedApp = null; - private GBDevice mGBDevice = null; - - private List getSystemApps() { - List 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 getCachedApps() { - List 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 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(); - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java index 37de8193..6b9178fb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenter.java @@ -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 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 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 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 availableDevices = DeviceHelper.getInstance().getAvailableDevices(this); - deviceList.retainAll(availableDevices); - for (GBDevice availableDevice : availableDevices) { - if (!deviceList.contains(availableDevice)) { - deviceList.add(availableDevice); - } - } - boolean connected = false; + List 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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DbManagementActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DbManagementActivity.java new file mode 100644 index 00000000..07e0d22a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DbManagementActivity.java @@ -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() { + @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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java index 3922505e..30d0e631 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java @@ -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 spinnerArray = new ArrayList<>(); + for (NotificationType notificationType : NotificationType.values()) { + spinnerArray.add(notificationType.name()); + } + ArrayAdapter 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); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java index 3f00ced4..b883fc1c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java @@ -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 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 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 getScanFilters() { + List 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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java index a5181dcc..856fe0ad 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ExternalPebbleJSActivity.java @@ -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 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 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/OnboardingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/OnboardingActivity.java new file mode 100644 index 00000000..043c3741 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/OnboardingActivity.java @@ -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() { + @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); + } + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java index 41d6bc55..990c9085 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java @@ -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 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); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/VibrationActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/VibrationActivity.java new file mode 100644 index 00000000..888fabae --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/VibrationActivity.java @@ -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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java new file mode 100644 index 00000000..3c0de5ae --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java @@ -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 getSystemAppsInCategory(); + + protected abstract String getSortFilename(); + + protected abstract boolean isCacheManager(); + + protected abstract boolean filterApp(GBDeviceApp gbDeviceApp); + + protected void onChangedAppOrder() { + List 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 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 appList = new ArrayList<>(); + private GBDeviceAppAdapter mGBDeviceAppAdapter; + protected GBDevice mGBDevice = null; + + protected List getCachedApps(List uuids) { + List 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 uuids = new ArrayList<>(); + for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getItemList()) { + uuids.add(gbDeviceApp.getUUID()); + } + if (concatFilename != null) { + ArrayList 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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerActivity.java new file mode 100644 index 00000000..d464d032 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerActivity.java @@ -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 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 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 uuids = getUuidsFromFile(filename); + if (!uuids.contains(uuid)) { + uuids.add(uuid); + rewriteAppOrderFile(filename, uuids); + } + } + + static synchronized ArrayList getUuidsFromFile(String filename) { + ArrayList 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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentCache.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentCache.java new file mode 100644 index 00000000..8423ffbe --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentCache.java @@ -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 getSystemAppsInCategory() { + return null; + } + + @Override + public String getSortFilename() { + return "pbwcacheorder.txt"; + } + + @Override + protected boolean filterApp(GBDeviceApp gbDeviceApp) { + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledApps.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledApps.java new file mode 100644 index 00000000..6cf8bfbb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledApps.java @@ -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 getSystemAppsInCategory() { + List systemApps = new ArrayList<>(); + //systemApps.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); + //systemApps.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM)); + systemApps.add(new GBDeviceApp(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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledWatchfaces.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledWatchfaces.java new file mode 100644 index 00000000..b41fad04 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AppManagerFragmentInstalledWatchfaces.java @@ -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 getSystemAppsInCategory() { + List 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java index b4c776ee..b1beeb77 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java @@ -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 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 getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { - SampleProvider provider = getProvider(device); - return db.getAllActivitySamples(tsFrom, tsTo, provider); + protected List getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + SampleProvider provider = getProvider(db, device); + return provider.getAllActivitySamples(tsFrom, tsTo); } - private int getTSLast24Hours(int tsTo) { - return (tsTo) - (24 * 60 * 60); // -24 hours - } - - protected List getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { - SampleProvider provider = getProvider(device); - return db.getActivitySamples(tsFrom, tsTo, provider); + protected List getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + SampleProvider provider = getProvider(db, device); + return provider.getActivitySamples(tsFrom, tsTo); } - protected List getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { - SampleProvider provider = getProvider(device); - return db.getSleepSamples(tsFrom, tsTo, provider); - } - - protected List 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 getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + SampleProvider 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 samples) { - Calendar cal = GregorianCalendar.getInstance(); - cal.clear(); - Date date; - String dateStringFrom = ""; - String dateStringTo = ""; + protected DefaultChartsData refresh(GBDevice gbDevice, List samples) { +// Calendar cal = GregorianCalendar.getInstance(); +// cal.clear(); + TimestampTranslation tsTranslation = new TimestampTranslation(); +// Date date; +// String dateStringFrom = ""; +// String dateStringTo = ""; +// ArrayList 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 xLabels = new ArrayList<>(numEntries); List activityEntries = new ArrayList<>(numEntries); - boolean hr = supportsHeartrate(); + boolean hr = supportsHeartrate(gbDevice); List heartrateEntries = hr ? new ArrayList(numEntries) : null; List 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 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.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 getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo); + protected abstract List 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 values, List 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 getSamples(DBHandler db, GBDevice device) { - return getSamples(db, device, getTSStart(), getTSEnd()); + protected List getSamples(DBHandler db, GBDevice device) { + int tsStart = getTSStart(); + int tsEnd = getTSEnd(); + List samples = (List) getSamples(db, device, tsStart, tsEnd); + ensureStartAndEndSamples(samples, tsStart, tsEnd); +// List 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 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> 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 xLabels; + + public PreformattedXIndexLabelFormatter(ArrayList 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; } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java index f656b058..8d0719f2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java @@ -8,7 +8,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; public class ActivityAnalysis { - public ActivityAmounts calculateActivityAmounts(List samples) { + public ActivityAmounts calculateActivityAmounts(List 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 samples) { + public int calculateTotalSteps(List samples) { int totalSteps = 0; for (ActivitySample sample : samples) { - totalSteps += sample.getSteps(); + int steps = sample.getSteps(); + if (steps > 0) { + totalSteps += sample.getSteps(); + } } return totalSteps; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java index 7260a205..43be544f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java @@ -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 samples = getSamples(db, device); + List 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 legendColors = new ArrayList<>(4); - List 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 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 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 getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { return getAllSamples(db, device, tsFrom, tsTo); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java index f09fca79..292202cc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java @@ -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)); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java index 19a62a4b..12b1d152 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java @@ -69,12 +69,13 @@ public class LiveActivityFragment extends AbstractChartFragment { private List 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 entries = new ArrayList<>(); - List xLabels = new ArrayList<>(); List 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(), 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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SingleEntryValueAnimator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SingleEntryValueAnimator.java index 3655f3ba..76476c73 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SingleEntryValueAnimator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SingleEntryValueAnimator.java @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java index 48ff18f8..5df0d587 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java @@ -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 samples = getSamples(db, device); + List 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 samples) { + private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List samples) { ActivityAnalysis analysis = new ActivityAnalysis(); ActivityAmounts amounts = analysis.calculateActivityAmounts(samples); PieData data = new PieData(); - List entries = new ArrayList<>(); + List entries = new ArrayList<>(); List 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 legendColors = new ArrayList<>(2); - List 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 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 getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + protected List 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 chartsData; private final MySleepChartsData pieData; - public MyChartsData(MySleepChartsData pieData, DefaultChartsData chartsData) { + public MyChartsData(MySleepChartsData pieData, DefaultChartsData chartsData) { this.pieData = pieData; this.chartsData = chartsData; } @@ -237,7 +252,7 @@ public class SleepChartFragment extends AbstractChartFragment { return pieData; } - public DefaultChartsData getChartsData() { + public DefaultChartsData getChartsData() { return chartsData; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java new file mode 100644 index 00000000..437330cb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TrailingActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TrailingActivitySample.java new file mode 100644 index 00000000..4c2fc77a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TrailingActivitySample.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java index 2e271f28..3d08ed5e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java @@ -6,8 +6,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.github.mikephil.charting.charts.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 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 entries = new ArrayList<>(); - List labels = new ArrayList<>(); + ArrayList labels = new ArrayList(); 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 entries = new ArrayList<>(); + List entries = new ArrayList<>(); List 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 legendColors = new ArrayList<>(1); // List legendLabels = new ArrayList<>(1); @@ -214,7 +214,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment { // chart.getLegend().setTextColor(LEGEND_TEXT_COLOR); } - private List getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) { + private List getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) { int startTs; int endTs; @@ -233,7 +233,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment { } @Override - protected List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + protected List 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 weekBeforeStepsData; private final DaySteps daySteps; - public MyChartsData(DaySteps daySteps, DefaultChartsData weekBeforeStepsData) { + public MyChartsData(DaySteps daySteps, DefaultChartsData weekBeforeStepsData) { this.daySteps = daySteps; this.weekBeforeStepsData = weekBeforeStepsData; } @@ -260,7 +260,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment { return daySteps; } - public DefaultChartsData getWeekBeforeStepsData() { + public DefaultChartsData getWeekBeforeStepsData() { return weekBeforeStepsData; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/DeviceCandidateAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/DeviceCandidateAdapter.java index 48252eb8..a80e3fac 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/DeviceCandidateAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/DeviceCandidateAdapter.java @@ -51,6 +51,7 @@ public class DeviceCandidateAdapter extends ArrayAdapter { deviceImageView.setImageResource(R.drawable.ic_device_pebble); break; case MIBAND: + case MIBAND2: deviceImageView.setImageResource(R.drawable.ic_device_miband); break; default: diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java index 92ca09b5..3e3a25a2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapter.java @@ -120,12 +120,20 @@ public class GBDeviceAdapter extends ArrayAdapter { } 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 { 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(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java index d4ae263f..35549ae8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java @@ -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 { - private final Context context; +public class GBDeviceAppAdapter extends DragItemAdapter { - public GBDeviceAppAdapter(Context context, List 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 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.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()); + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java index ce9e1ae4..221cf410 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/ActivityDatabaseHandler.java @@ -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 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 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 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 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 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(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBAccess.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBAccess.java index ed42b28d..4504393a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBAccess.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBAccess.java @@ -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; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java index 5d1a72b4..671b5af3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBConstants.java @@ -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"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java index 49dbbd40..d4f09125 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHandler.java @@ -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 getAllActivitySamples(int tsFrom, int tsTo, SampleProvider provider); - - List getActivitySamples(int tsFrom, int tsTo, SampleProvider provider); - - List 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(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java index 7c53d326..35f0efbf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBHelper.java @@ -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. + *

+ * 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 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 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 = 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, 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 query = deviceDao.queryBuilder().where(DeviceDao.Properties.Identifier.eq(gbDevice.getAddress())).build(); + List 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 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 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 = 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, 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 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 qb = session.getActivityDescriptionDao().queryBuilder(); + qb.where(userIdProperty.eq(user.getId()), isAtLeastPartiallyInRange(qb, tsFromProperty, tsToProperty, tsFrom, tsTo)); + List 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 the query builder's type parameter + * @return the range WhereCondition + */ + private static WhereCondition isAtLeastPartiallyInRange(QueryBuilder 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 qb = tagDao.queryBuilder(); + Query query = qb.where(TagDao.Properties.UserId.eq(user.getId()), TagDao.Properties.Name.eq(name)).build(); + List 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 sampleProvider = (AbstractSampleProvider) coordinator.getSampleProvider(targetDevice, session); + importActivitySamples(oldDB, targetDevice, session, sampleProvider, user); + break; + } + } + } + } + + private void importActivitySamples(SQLiteDatabase fromDb, GBDevice targetDevice, DaoSession targetSession, AbstractSampleProvider 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 overlayList = new ArrayList<>(); + + final int BATCH_SIZE = 100000; // 100.000 samples = rougly 20 MB per batch + List 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); + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBOpenHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBOpenHelper.java new file mode 100644 index 00000000..9a61ae7b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/DBOpenHelper.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_X.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_X.java deleted file mode 100644 index e23d13e5..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/ActivityDBUpdate_X.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_14.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_14.java new file mode 100644 index 00000000..f7c64ebe --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_14.java @@ -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) { + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_15.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_15.java new file mode 100644 index 00000000..615f1b8d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_15.java @@ -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) { + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/SchemaMigration.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/SchemaMigration.java new file mode 100644 index 00000000..40c6df15 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/SchemaMigration.java @@ -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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java index 47321039..2fc59fd8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java @@ -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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index 28e5fcf8..46a65e09 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -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 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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java new file mode 100644 index 00000000..781548f1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java @@ -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 the sample type + */ +public abstract class AbstractSampleProvider implements SampleProvider { + 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 getAllActivitySamples(int timestamp_from, int timestamp_to) { + return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL); + } + + @Override + public List 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 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 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 samples = qb.build().list(); + if (samples.isEmpty()) { + return null; + } + T sample = samples.get(0); + sample.setProvider(this); + return sample; + } + + protected List 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 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 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 getActivitySamplesByActivityFilter(int timestamp_from, int timestamp_to, int activityFilter) { + List samples = getAllActivitySamples(timestamp_from, timestamp_to); + List filteredSamples = new ArrayList<>(); + + for (T sample : samples) { + if ((sample.getKind() & activityFilter) != 0) { + filteredSamples.add(sample); + } + } + return filteredSamples; + } + + public abstract AbstractDao getSampleDao(); + + @Nullable + protected abstract Property getRawKindSampleProperty(); + + @NonNull + protected abstract Property getTimestampSampleProperty(); + + @NonNull + protected abstract Property getDeviceIdentifierSampleProperty(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index 7ecc3e8d..6f0fb39d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -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 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 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 getAppsManagementActivity(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceManager.java new file mode 100644 index 00000000..1db080a2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceManager.java @@ -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 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 availableDevices = DeviceHelper.getInstance().getAvailableDevices(context); + deviceList.retainAll(availableDevices); + for (GBDevice availableDevice : availableDevices) { + if (!deviceList.contains(availableDevice)) { + deviceList.add(availableDevice); + } + } + + Collections.sort(deviceList, new Comparator() { + @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 getDevices() { + return Collections.unmodifiableList(deviceList); + } + + @Nullable + public GBDevice getSelectedDevice() { + return selectedDevice; + } + + private void notifyDevicesChanged() { + LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(ACTION_DEVICES_CHANGED)); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java index a275ace5..2e652788 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java @@ -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(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java index 78463d02..fca1710e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java @@ -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 the device/provider specific sample type (must extend AbstractActivitySample) + */ +public interface SampleProvider { + // 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 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 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 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(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java index 1af8da27..6a50ba0f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java @@ -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 getAppsManagementActivity() { + return null; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewConstants.java new file mode 100644 index 00000000..48786c24 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewConstants.java @@ -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 (http://adq.livejournal.com). + * + * @author Robert <xperimental@solidproject.de> + */ +public final class LiveviewConstants { + + public static Charset ENCODING = StandardCharsets.ISO_8859_1; + public static ByteOrder BYTE_ORDER = ByteOrder.BIG_ENDIAN; + + public static final byte CLOCK_24H = 0; + public static final byte CLOCK_12H = 1; + + public static final byte MSG_GETCAPS = 1; + public static final byte MSG_GETCAPS_RESP = 2; + + public static final byte MSG_DISPLAYTEXT = 3; + public static final byte MSG_DISPLAYTEXT_ACK = 4; + + public static final byte MSG_DISPLAYPANEL = 5; + public static final byte MSG_DISPLAYPANEL_ACK = 6; + + public static final byte MSG_DEVICESTATUS = 7; + public static final byte MSG_DEVICESTATUS_ACK = 8; + + public static final byte MSG_DISPLAYBITMAP = 19; + public static final byte MSG_DISPLAYBITMAP_ACK = 20; + + public static final byte MSG_CLEARDISPLAY = 21; + public static final byte MSG_CLEARDISPLAY_ACK = 22; + + public static final byte MSG_SETMENUSIZE = 23; + public static final byte MSG_SETMENUSIZE_ACK = 24; + + public static final byte MSG_GETMENUITEM = 25; + public static final byte MSG_GETMENUITEM_RESP = 26; + + public static final byte MSG_GETALERT = 27; + public static final byte MSG_GETALERT_RESP = 28; + + public static final byte MSG_NAVIGATION = 29; + public static final byte MSG_NAVIGATION_RESP = 30; + + public static final byte MSG_SETSTATUSBAR = 33; + public static final byte MSG_SETSTATUSBAR_ACK = 34; + + public static final byte MSG_GETMENUITEMS = 35; + + public static final byte MSG_SETMENUSETTINGS = 36; + public static final byte MSG_SETMENUSETTINGS_ACK = 37; + + public static final byte MSG_GETTIME = 38; + public static final byte MSG_GETTIME_RESP = 39; + + public static final byte MSG_SETLED = 40; + public static final byte MSG_SETLED_ACK = 41; + + public static final byte MSG_SETVIBRATE = 42; + public static final byte MSG_SETVIBRATE_ACK = 43; + + public static final byte MSG_ACK = 44; + + public static final byte MSG_SETSCREENMODE = 64; + public static final byte MSG_SETSCREENMODE_ACK = 65; + + public static final byte MSG_GETSCREENMODE = 66; + public static final byte MSG_GETSCREENMODE_RESP = 67; + + public static final int DEVICESTATUS_OFF = 0; + public static final int DEVICESTATUS_ON = 1; + public static final int DEVICESTATUS_MENU = 2; + + public static final byte RESULT_OK = 0; + public static final byte RESULT_ERROR = 1; + public static final byte RESULT_OOM = 2; + public static final byte RESULT_EXIT = 3; + public static final byte RESULT_CANCEL = 4; + + public static final int NAVACTION_PRESS = 0; + public static final int NAVACTION_LONGPRESS = 1; + public static final int NAVACTION_DOUBLEPRESS = 2; + + public static final int NAVTYPE_UP = 0; + public static final int NAVTYPE_DOWN = 1; + public static final int NAVTYPE_LEFT = 2; + public static final int NAVTYPE_RIGHT = 3; + public static final int NAVTYPE_SELECT = 4; + public static final int NAVTYPE_MENUSELECT = 5; + + public static final int ALERTACTION_CURRENT = 0; + public static final int ALERTACTION_FIRST = 1; + public static final int ALERTACTION_LAST = 2; + public static final int ALERTACTION_NEXT = 3; + public static final int ALERTACTION_PREV = 4; + + public static final int BRIGHTNESS_OFF = 49; + public static final int BRIGHTNESS_DIM = 50; + public static final int BRIGHTNESS_MAX = 51; + + public static final String CLIENT_SOFTWARE_VERSION = "0.0.3"; + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewCoordinator.java new file mode 100644 index 00000000..338137ce --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewCoordinator.java @@ -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 getPairingActivity() { + return null; + } + + @Override + public Class 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 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 getAppsManagementActivity() { + return null; + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + // nothing to delete, yet + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandSampleProvider.java new file mode 100644 index 00000000..36a87b2e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandSampleProvider.java @@ -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 { + + // 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 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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/DateTimeDisplay.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/DateTimeDisplay.java new file mode 100644 index 00000000..40857aa4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/DateTimeDisplay.java @@ -0,0 +1,6 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.miband; + +public enum DateTimeDisplay { + TIME, + DATE_TIME +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java new file mode 100644 index 00000000..5d40bfc1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java @@ -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 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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2SampleProvider.java new file mode 100644 index 00000000..c7debdcf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2SampleProvider.java @@ -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 getGBActivitySamples(int timestamp_from, int timestamp_to, int activityType) { + List 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 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 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 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; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java new file mode 100644 index 00000000..a0b04af1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java @@ -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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java index ca8b9e64..f4d5ec9f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java @@ -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"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java index 505d3530..abff3bc1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java @@ -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 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 getPrimaryActivity() { return ChartsActivity.class; } @Override - public SampleProvider getSampleProvider() { - return sampleProvider; + public SampleProvider 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 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); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java index 20ff39d9..04f87c71 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java @@ -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 }; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java index 12d88683..89424a1f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java @@ -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); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java index 7fb29ce9..00b77c06 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java @@ -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 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]); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java index b87406e1..8f2a95c7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandSampleProvider.java @@ -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; - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java index 39d5bd69..51d51c4e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java @@ -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 MIBAND_DEBUG; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/VibrationProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/VibrationProfile.java index 97dfc4e6..b23342bc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/VibrationProfile.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/VibrationProfile.java @@ -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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java deleted file mode 100644 index b62abe16..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/HealthSampleProvider.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MisfitSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MisfitSampleProvider.java deleted file mode 100644 index bce42a37..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MisfitSampleProvider.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MorpheuzSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MorpheuzSampleProvider.java deleted file mode 100644 index 1867c341..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/MorpheuzSampleProvider.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java index 9d66c0db..70ed7f37 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWInstallHandler.java @@ -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; } -} \ No newline at end of file +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java index 42f05b74..3e3b4710 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PBWReader.java @@ -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; } -} \ No newline at end of file +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java index c74db054..5953113d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java @@ -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 getPairingActivity() { - return null; + return PebblePairingActivity.class; } + @Override public Class 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 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 getAppsManagementActivity() { + return AppManagerActivity.class; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleGadgetBridgeSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleGadgetBridgeSampleProvider.java deleted file mode 100644 index b047fdb4..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleGadgetBridgeSampleProvider.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleHealthSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleHealthSampleProvider.java new file mode 100644 index 00000000..efff6a26 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleHealthSampleProvider.java @@ -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 { + 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 getAllActivitySamples(int timestamp_from, int timestamp_to) { + List 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 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 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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleIconID.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleIconID.java index 18b409ae..89069a8c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleIconID.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleIconID.java @@ -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; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMisfitSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMisfitSampleProvider.java new file mode 100644 index 00000000..98bc244c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMisfitSampleProvider.java @@ -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 { + + 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 getSampleDao() { + return getSession().getPebbleMisfitSampleDao(); + } + + @Override + protected Property getRawKindSampleProperty() { + return null; + } + + protected Property getTimestampSampleProperty() { + return PebbleMisfitSampleDao.Properties.Timestamp; + } + + protected Property getDeviceIdentifierSampleProperty() { + return PebbleMisfitSampleDao.Properties.DeviceId; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMorpheuzSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMorpheuzSampleProvider.java new file mode 100644 index 00000000..8bdf8f01 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleMorpheuzSampleProvider.java @@ -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 { + + protected float movementDivisor = 5000f; + + public PebbleMorpheuzSampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @Override + public AbstractDao 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java new file mode 100644 index 00000000..2cf3b545 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java @@ -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 query = deviceDao.queryBuilder().where(DeviceDao.Properties.Type.eq(1), DeviceDao.Properties.Identifier.like("%" + expectedSuffix)).build(); + List 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vibratissimo/VibratissimoCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vibratissimo/VibratissimoCoordinator.java new file mode 100644 index 00000000..8f71f234 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vibratissimo/VibratissimoCoordinator.java @@ -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 getPairingActivity() { + return null; + } + + @Override + public Class 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 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 getAppsManagementActivity() { + return null; + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + // nothing to delete, yet + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/.gitignore b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/.gitignore new file mode 100644 index 00000000..4d5306db --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/.gitignore @@ -0,0 +1 @@ +*.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractActivitySample.java new file mode 100644 index 00000000..03f9e442 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractActivitySample.java @@ -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() + + '}'; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleHealthActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleHealthActivitySample.java new file mode 100644 index 00000000..79f277b1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleHealthActivitySample.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMisfitActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMisfitActivitySample.java new file mode 100644 index 00000000..53e47c1a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMisfitActivitySample.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMorpheuzActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMorpheuzActivitySample.java new file mode 100644 index 00000000..4a47d7b1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractPebbleMorpheuzActivitySample.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmReceiver.java index e9470d31..9c4a581a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmReceiver.java @@ -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(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java index 73d26d0d..0e639733 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothStateChangeReceiver.java @@ -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(); } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java index 439c34b0..c9431b43 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/K9Receiver.java @@ -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, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java index 67c7e8e7..3edfb881 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java @@ -3,30 +3,54 @@ package nodomain.freeyourgadget.gadgetbridge.externalevents; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.os.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; public class MusicPlaybackReceiver extends BroadcastReceiver { private static final Logger LOG = LoggerFactory.getLogger(MusicPlaybackReceiver.class); + private static MusicSpec lastMusicSpec = new MusicSpec(); + private static MusicStateSpec lastStatecSpec = new MusicStateSpec(); @Override public void onReceive(Context context, Intent intent) { - String artist = intent.getStringExtra("artist"); - String album = intent.getStringExtra("album"); - String track = intent.getStringExtra("track"); - - LOG.info("Current track: " + artist + ", " + album + ", " + track); - + /* + Bundle bundle = intent.getExtras(); + for (String key : bundle.keySet()) { + Object value = bundle.get(key); + LOG.info(String.format("%s %s (%s)", key, + value != null ? value.toString() : "null", value != null ? value.getClass().getName() : "no class")); + } + */ MusicSpec musicSpec = new MusicSpec(); - musicSpec.artist = artist; - musicSpec.album = album; - musicSpec.track = track; + musicSpec.artist = intent.getStringExtra("artist"); + musicSpec.album = intent.getStringExtra("album"); + musicSpec.track = intent.getStringExtra("track"); + musicSpec.duration = intent.getIntExtra("duration", 0) / 1000; - GBApplication.deviceService().onSetMusicInfo(musicSpec); + if (!lastMusicSpec.equals(musicSpec)) { + lastMusicSpec = musicSpec; + LOG.info("Update Music Info: " + musicSpec.artist + " / " + musicSpec.album + " / " + musicSpec.track); + GBApplication.deviceService().onSetMusicInfo(musicSpec); + } else { + LOG.info("got metadata changed intent, but nothing changed, ignoring."); + } + + if (intent.hasExtra("position") && intent.hasExtra("playing")) { + MusicStateSpec stateSpec = new MusicStateSpec(); + stateSpec.position = intent.getIntExtra("position", 0) / 1000; + stateSpec.state = (byte) (intent.getBooleanExtra("playing", true) ? MusicStateSpec.STATE_PLAYING : MusicStateSpec.STATE_PAUSED); + if (!lastStatecSpec.equals(stateSpec)) { + LOG.info("Update Music State: state=" + stateSpec.state + ", position= " + stateSpec.position); + GBApplication.deviceService().onSetMusicState(stateSpec); + } else { + LOG.info("got state changed intent, but not enough has changed, ignoring."); + } + lastStatecSpec = stateSpec; + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index d8e0f021..ca1ef0e1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -18,7 +18,6 @@ import android.media.session.PlaybackState; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; -import android.provider.MediaStore; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.support.v4.app.NotificationCompat; @@ -31,6 +30,7 @@ import org.slf4j.LoggerFactory; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.model.AppNotificationType; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; @@ -194,6 +194,11 @@ public class NotificationListener extends NotificationListenerService { } } + //don't forward group summary notifications to the wearable, they are meant for the android device only + if ((notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) { + return; + } + if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) { return; } @@ -210,12 +215,6 @@ public class NotificationListener extends NotificationListenerService { return; } - if (source.equals("eu.siacs.conversations")) { - if (!"never".equals(prefs.getString("notification_mode_pebblemsg", "when_screen_off"))) { - return; - } - } - if (source.equals("com.fsck.k9")) { if (!"never".equals(prefs.getString("notification_mode_k9mail", "when_screen_off"))) { return; @@ -250,43 +249,22 @@ public class NotificationListener extends NotificationListenerService { notificationSpec.sourceName = (String) pm.getApplicationLabel(ai); } - switch (source) { - case "org.mariotaku.twidere": - case "com.twitter.android": - case "org.andstatus.app": - case "org.mustard.android": - notificationSpec.type = NotificationType.TWITTER; - break; - case "com.fsck.k9": - case "com.android.email": - notificationSpec.type = NotificationType.EMAIL; - break; - case "com.moez.QKSMS": - case "com.android.mms": - case "com.android.messaging": - case "com.sonyericsson.conversations": - case "org.smssecure.smssecure": - notificationSpec.type = NotificationType.SMS; - break; - case "eu.siacs.conversations": - case "org.thoughtcrime.securesms": - notificationSpec.type = NotificationType.CHAT; - break; - case "org.indywidualni.fblite": - notificationSpec.type = NotificationType.FACEBOOK; - break; - default: - notificationSpec.type = NotificationType.UNDEFINED; - break; - } + boolean preferBigText = false; + + notificationSpec.type = AppNotificationType.getInstance().get(source); LOG.info("Processing notification from source " + source); - dissectNotificationTo(notification, notificationSpec); + dissectNotificationTo(notification, notificationSpec, preferBigText); notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification); List actions = wearableExtender.getActions(); + + if (actions.isEmpty() && notificationSpec.type == NotificationType.TELEGRAM) { + return; // workaround for duplicate telegram message + } + for (NotificationCompat.Action act : actions) { if (act != null && act.getRemoteInputs() != null) { LOG.info("found wearable action: " + act.getTitle() + " " + sbn.getTag()); @@ -299,18 +277,26 @@ public class NotificationListener extends NotificationListenerService { GBApplication.deviceService().onNotification(notificationSpec); } - private void dissectNotificationTo(Notification notification, NotificationSpec notificationSpec) { + private void dissectNotificationTo(Notification notification, NotificationSpec notificationSpec, boolean preferBigText) { Bundle extras = notification.extras; + + //dumpExtras(extras); + CharSequence title = extras.getCharSequence(Notification.EXTRA_TITLE); if (title != null) { notificationSpec.title = title.toString(); } - if (extras.containsKey(Notification.EXTRA_TEXT)) { - CharSequence contentCS = extras.getCharSequence(Notification.EXTRA_TEXT); - if (contentCS != null) { - notificationSpec.body = contentCS.toString(); - } + + CharSequence contentCS = null; + if (preferBigText && extras.containsKey(Notification.EXTRA_BIG_TEXT)) { + contentCS = extras.getCharSequence(Notification.EXTRA_BIG_TEXT); + } else if (extras.containsKey(Notification.EXTRA_TEXT)) { + contentCS = extras.getCharSequence(Notification.EXTRA_TEXT); } + if (contentCS != null) { + notificationSpec.body = contentCS.toString(); + } + } private boolean isServiceRunning() { @@ -354,7 +340,7 @@ public class NotificationListener extends NotificationListenerService { } PlaybackState s = c.getPlaybackState(); - stateSpec.position = (int)s.getPosition(); + stateSpec.position = (int) (s.getPosition() / 1000); stateSpec.playRate = Math.round(100 * s.getPlaybackSpeed()); stateSpec.repeat = 1; stateSpec.shuffle = 1; @@ -384,6 +370,10 @@ public class NotificationListener extends NotificationListenerService { musicSpec.track = d.getString(MediaMetadata.METADATA_KEY_TITLE); if (d.containsKey(MediaMetadata.METADATA_KEY_DURATION)) musicSpec.duration = (int)d.getLong(MediaMetadata.METADATA_KEY_DURATION) / 1000; + if (d.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) + musicSpec.trackCount = (int)d.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS); + if (d.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) + musicSpec.trackNr = (int)d.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER); // finally, tell the device about it GBApplication.deviceService().onSetMusicInfo(musicSpec); @@ -396,4 +386,14 @@ public class NotificationListener extends NotificationListenerService { public void onNotificationRemoved(StatusBarNotification sbn) { } + + private void dumpExtras(Bundle bundle) { + for (String key : bundle.keySet()) { + Object value = bundle.get(key); + if (value == null) { + continue; + } + LOG.debug(String.format("Notification extra: %s %s (%s)", key, value.toString(), value.getClass().getName())); + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java index 801c151b..723f5c4a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PebbleReceiver.java @@ -58,10 +58,13 @@ public class PebbleReceiver extends BroadcastReceiver { } if (notificationSpec.title != null) { - notificationSpec.type = NotificationType.UNDEFINED; + notificationSpec.type = NotificationType.UNKNOWN; String sender = intent.getStringExtra("sender"); if ("Conversations".equals(sender)) { - notificationSpec.type = NotificationType.CHAT; + notificationSpec.type = NotificationType.CONVERSATIONS; + } + else if ("OsmAnd".equals(sender)) { + notificationSpec.type = NotificationType.GENERIC_NAVIGATION; } GBApplication.deviceService().onNotification(notificationSpec); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java index 5df07c8d..62e66a26 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/SMSReceiver.java @@ -31,7 +31,7 @@ public class SMSReceiver extends BroadcastReceiver { NotificationSpec notificationSpec = new NotificationSpec(); notificationSpec.id = -1; - notificationSpec.type = NotificationType.SMS; + notificationSpec.type = NotificationType.GENERIC_SMS; Bundle bundle = intent.getExtras(); if (bundle != null) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBActivitySample.java deleted file mode 100644 index 895b903b..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBActivitySample.java +++ /dev/null @@ -1,94 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.impl; - -import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; - -public class GBActivitySample implements ActivitySample { - private final int timestamp; - private final SampleProvider provider; - private final int intensity; - private final int steps; - private final int type; - private final int customValue; - - public GBActivitySample(SampleProvider provider, int timestamp, int intensity, int steps, int type) { - this(provider, timestamp, intensity, steps, type, 0); - } - - public GBActivitySample(SampleProvider provider, int timestamp, int intensity, int steps, int type, int customValue) { - this.timestamp = timestamp; - this.provider = provider; - this.intensity = intensity; - this.steps = steps; - this.customValue = customValue; - this.type = type; - validate(); - } - - private void validate() { - if (steps < 0) { - throw new IllegalArgumentException("steps must be >= 0"); - } - if (intensity < 0) { - throw new IllegalArgumentException("intensity must be >= 0"); - } - if (timestamp < 0) { - throw new IllegalArgumentException("timestamp must be >= 0"); - } - if (customValue < 0) { - throw new IllegalArgumentException("customValue must be >= 0"); - } - } - - @Override - public int getTimestamp() { - return timestamp; - } - - @Override - public SampleProvider getProvider() { - return provider; - } - - @Override - public int getRawIntensity() { - return intensity; - } - - @Override - public float getIntensity() { - return getProvider().normalizeIntensity(getRawIntensity()); - } - - @Override - public int getSteps() { - return steps; - } - - @Override - public int getRawKind() { - return type; - } - - @Override - public int getKind() { - return getProvider().normalizeType(getRawKind()); - } - - @Override - public int getCustomValue() { - return customValue; - } - - @Override - public String toString() { - return "GBActivitySample{" + - "timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimeStamp(timestamp)) + - ", intensity=" + getIntensity() + - ", steps=" + getSteps() + - ", customValue=" + getCustomValue() + - ", type=" + getKind() + - '}'; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java index 546bcee2..54ac2e31 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBAlarm.java @@ -27,7 +27,7 @@ public class GBAlarm implements Alarm { public static final String[] DEFAULT_ALARMS = {"2,false,false,0,15,30", "1,false,false,96,8,0", "0,false,true,31,7,30"}; - public GBAlarm(int index, boolean enabled, boolean smartWakeup, byte repetition, int hour, int minute) { + public GBAlarm(int index, boolean enabled, boolean smartWakeup, int repetition, int hour, int minute) { this.index = index; this.enabled = enabled; this.smartWakeup = smartWakeup; @@ -47,6 +47,10 @@ public class GBAlarm implements Alarm { this.minute = Integer.parseInt(tokens[5]); } + public static GBAlarm createSingleShot(int index, boolean smartWakeup, Calendar calendar) { + return new GBAlarm(index, true, smartWakeup, Alarm.ALARM_ONCE, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE)); + } + private static GBAlarm readFromParcel(Parcel pc) { int index = pc.readInt(); boolean enabled = Boolean.parseBoolean(pc.readString()); @@ -54,7 +58,7 @@ public class GBAlarm implements Alarm { int repetition = pc.readInt(); int hour = pc.readInt(); int minute = pc.readInt(); - return new GBAlarm(index, enabled, smartWakeup, (byte) repetition, hour, minute); + return new GBAlarm(index, enabled, smartWakeup, repetition, hour, minute); } @Override @@ -149,6 +153,11 @@ public class GBAlarm implements Alarm { return this.repetition; } + @Override + public boolean isRepetitive() { + return getRepetitionMask() != ALARM_ONCE; + } + public String toPreferences() { return String.valueOf(this.index) + ',' + String.valueOf(this.enabled) + ',' + @@ -205,10 +214,12 @@ public class GBAlarm implements Alarm { } public static final Creator CREATOR = new Creator() { + @Override public GBAlarm createFromParcel(Parcel in) { return readFromParcel(in); } + @Override public GBAlarm[] newArray(int size) { return new GBAlarm[size]; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java index 25275534..29f61d76 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.content.LocalBroadcastManager; import org.slf4j.Logger; @@ -42,12 +43,16 @@ public class GBDevice implements Parcelable { public static final String EXTRA_DEVICE = "device"; private static final String DEVINFO_HW_VER = "HW: "; private static final String DEVINFO_FW_VER = "FW: "; + private static final String DEVINFO_HR_VER = "HR: "; private static final String DEVINFO_ADDR = "ADDR: "; - private final String mName; + private static final String DEVINFO_ADDR2 = "ADDR2: "; + private String mName; private final String mAddress; + private String mVolatileAddress; private final DeviceType mDeviceType; - private String mFirmwareVersion = null; - private String mHardwareVersion = null; + private String mFirmwareVersion; + private String mFirmwareVersion2; + private String mModel; private State mState = State.NOT_CONNECTED; private short mBatteryLevel = BATTERY_UNKNOWN; private short mBatteryThresholdPercent = BATTERY_THRESHOLD_PERCENT; @@ -57,8 +62,13 @@ public class GBDevice implements Parcelable { private List mDeviceInfos; public GBDevice(String address, String name, DeviceType deviceType) { + this(address, null, name, deviceType); + } + + public GBDevice(String address, String address2, String name, DeviceType deviceType) { mAddress = address; - mName = name; + mVolatileAddress = address2; + mName = (name != null) ? name : mAddress; mDeviceType = deviceType; validate(); } @@ -66,9 +76,11 @@ public class GBDevice implements Parcelable { private GBDevice(Parcel in) { mName = in.readString(); mAddress = in.readString(); + mVolatileAddress = in.readString(); mDeviceType = DeviceType.values()[in.readInt()]; mFirmwareVersion = in.readString(); - mHardwareVersion = in.readString(); + mFirmwareVersion2 = in.readString(); + mModel = in.readString(); mState = State.values()[in.readInt()]; mBatteryLevel = (short) in.readInt(); mBatteryThresholdPercent = (short) in.readInt(); @@ -84,9 +96,11 @@ public class GBDevice implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeString(mName); dest.writeString(mAddress); + dest.writeString(mVolatileAddress); dest.writeInt(mDeviceType.ordinal()); dest.writeString(mFirmwareVersion); - dest.writeString(mHardwareVersion); + dest.writeString(mFirmwareVersion2); + dest.writeString(mModel); dest.writeInt(mState.ordinal()); dest.writeInt(mBatteryLevel); dest.writeInt(mBatteryThresholdPercent); @@ -106,24 +120,58 @@ public class GBDevice implements Parcelable { return mName; } + public void setName(String name) { + if (name == null) { + LOG.warn("Ignoring setting of GBDevice name to null for " + this); + return; + } + mName = name; + } + public String getAddress() { return mAddress; } + public String getVolatileAddress() { + return mVolatileAddress; + } + public String getFirmwareVersion() { return mFirmwareVersion; } + public String getFirmwareVersion2() { + return mFirmwareVersion2; + } public void setFirmwareVersion(String firmwareVersion) { mFirmwareVersion = firmwareVersion; } - public String getHardwareVersion() { - return mHardwareVersion; + /** + * Sets the second firmware version, typically the heart rate firmware version + * @param firmwareVersion2 + */ + public void setFirmwareVersion2(String firmwareVersion2) { + mFirmwareVersion2 = firmwareVersion2; } - public void setHardwareVersion(String hardwareVersion) { - mHardwareVersion = hardwareVersion; + public void setVolatileAddress(String volatileAddress) { + mVolatileAddress = volatileAddress; + } + + /** + * Returns the specific model/hardware revision of this device. + * This information is not always available, typically only when the device is initialized + * @return the model/hardware revision of this device + * @see #getType() + */ + @Nullable + public String getModel() { + return mModel; + } + + public void setModel(String model) { + mModel = model; } public boolean isConnected() { @@ -197,6 +245,7 @@ public class GBDevice implements Parcelable { setBatteryLevel(BATTERY_UNKNOWN); setBatteryState(BatteryState.UNKNOWN); setFirmwareVersion(null); + setFirmwareVersion2(null); setRssi(RSSI_UNKNOWN); if (mBusyTask != null) { unsetBusyTask(); @@ -204,29 +253,51 @@ public class GBDevice implements Parcelable { } public String getStateString() { - /* - * for simplicity the user wont see all internal states, just connecting -> connected - * instead of connecting->connected->initializing->initialized - */ + return getStateString(true); + } + + /** + * for simplicity the user won't see all internal states, just connecting -> connected + * instead of connecting->connected->initializing->initialized + * Set simple to true to get this behavior. + */ + private String getStateString(boolean simple) { switch (mState) { case NOT_CONNECTED: return GBApplication.getContext().getString(R.string.not_connected); case WAITING_FOR_RECONNECT: return GBApplication.getContext().getString(R.string.waiting_for_reconnect); case CONNECTING: - case CONNECTED: - case INITIALIZING: return GBApplication.getContext().getString(R.string.connecting); + case CONNECTED: + if (simple) { + return GBApplication.getContext().getString(R.string.connecting); + } + return GBApplication.getContext().getString(R.string.connected); + case INITIALIZING: + if (simple) { + return GBApplication.getContext().getString(R.string.connecting); + } + return GBApplication.getContext().getString(R.string.initializing); case AUTHENTICATION_REQUIRED: return GBApplication.getContext().getString(R.string.authentication_required); case AUTHENTICATING: return GBApplication.getContext().getString(R.string.authenticating); case INITIALIZED: - return GBApplication.getContext().getString(R.string.connected); + if (simple) { + return GBApplication.getContext().getString(R.string.connected); + } + return GBApplication.getContext().getString(R.string.initialized); } return GBApplication.getContext().getString(R.string.unknown_state); } + /** + * Returns the general type of this device. For more detailed information, + * soo #getModel() + * @return the general type of this device + */ + @NonNull public DeviceType getType() { return mDeviceType; } @@ -313,7 +384,7 @@ public class GBDevice implements Parcelable { @Override public String toString() { - return "Device " + getName() + ", " + getAddress() + ", " + getStateString(); + return "Device " + getName() + ", " + getAddress() + ", " + getStateString(false); } /** @@ -336,20 +407,35 @@ public class GBDevice implements Parcelable { return getDeviceInfos().size() > 0; } + public ItemWithDetails getDeviceInfo(String name) { + for (ItemWithDetails item : getDeviceInfos()) { + if (name.equals(item.getName())) { + return item; + } + } + return null; + } + public List getDeviceInfos() { List result = new ArrayList<>(); if (mDeviceInfos != null) { result.addAll(mDeviceInfos); } - if (mHardwareVersion != null) { - result.add(new GenericItem(DEVINFO_HW_VER, mHardwareVersion)); + if (mModel != null) { + result.add(new GenericItem(DEVINFO_HW_VER, mModel)); } if (mFirmwareVersion != null) { result.add(new GenericItem(DEVINFO_FW_VER, mFirmwareVersion)); } + if (mFirmwareVersion2 != null) { + result.add(new GenericItem(DEVINFO_HR_VER, mFirmwareVersion2)); + } if (mAddress != null) { result.add(new GenericItem(DEVINFO_ADDR, mAddress)); } + if (mVolatileAddress != null) { + result.add(new GenericItem(DEVINFO_ADDR2, mVolatileAddress)); + } Collections.sort(result); return result; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java index f5587645..5cbd0fab 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceApp.java @@ -89,6 +89,7 @@ public class GBDeviceApp { public enum Type { UNKNOWN, WATCHFACE, + WATCHFACE_SYSTEM, APP_GENERIC, APP_ACTIVITYTRACKER, APP_SYSTEM, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceCandidate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceCandidate.java index d0755636..3696ec5d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceCandidate.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceCandidate.java @@ -2,11 +2,24 @@ package nodomain.freeyourgadget.gadgetbridge.impl; import android.bluetooth.BluetoothDevice; import android.os.Parcel; +import android.os.ParcelUuid; import android.os.Parcelable; +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; /** * A device candidate is a Bluetooth device that is not yet managed by @@ -14,23 +27,29 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; * support this candidate, will the candidate be promoted to a GBDevice. */ public class GBDeviceCandidate implements Parcelable { + private static final Logger LOG = LoggerFactory.getLogger(GBDeviceCandidate.class); + private final BluetoothDevice device; private final short rssi; + private final ParcelUuid[] serviceUuds; private DeviceType deviceType = DeviceType.UNKNOWN; - public GBDeviceCandidate(BluetoothDevice device, short rssi) { + public GBDeviceCandidate(BluetoothDevice device, short rssi, ParcelUuid[] serviceUuds) { this.device = device; this.rssi = rssi; + this.serviceUuds = mergeServiceUuids(serviceUuds, device.getUuids()); } private GBDeviceCandidate(Parcel in) { device = in.readParcelable(getClass().getClassLoader()); + if (device == null) { + throw new IllegalStateException("Unable to read state from Parcel"); + } rssi = (short) in.readInt(); deviceType = DeviceType.valueOf(in.readString()); - if (device == null || deviceType == null) { - throw new IllegalStateException("Unable to read state from Parcel"); - } + ParcelUuid[] uuids = AndroidUtils.toParcelUUids(in.readParcelableArray(getClass().getClassLoader())); + serviceUuds = mergeServiceUuids(uuids, device.getUuids()); } @Override @@ -38,6 +57,27 @@ public class GBDeviceCandidate implements Parcelable { dest.writeParcelable(device, 0); dest.writeInt(rssi); dest.writeString(deviceType.name()); + dest.writeArray(serviceUuds); + } + + public static final Creator CREATOR = new Creator() { + @Override + public GBDeviceCandidate createFromParcel(Parcel in) { + return new GBDeviceCandidate(in); + } + + @Override + public GBDeviceCandidate[] newArray(int size) { + return new GBDeviceCandidate[size]; + } + }; + + public BluetoothDevice getDevice() { + return device; + } + + public void setDeviceType(DeviceType type) { + deviceType = type; } public DeviceType getDeviceType() { @@ -48,15 +88,54 @@ public class GBDeviceCandidate implements Parcelable { return device != null ? device.getAddress() : GBApplication.getContext().getString(R.string._unknown_); } + private ParcelUuid[] mergeServiceUuids(ParcelUuid[] serviceUuds, ParcelUuid[] deviceUuids) { + Set uuids = new HashSet<>(); + if (serviceUuds != null) { + uuids.addAll(Arrays.asList(serviceUuds)); + } + if (deviceUuids != null) { + uuids.addAll(Arrays.asList(deviceUuids)); + } + return uuids.toArray(new ParcelUuid[0]); + } + + @NonNull + public ParcelUuid[] getServiceUuids() { + return serviceUuds; + } + + public boolean supportsService(UUID aService) { + ParcelUuid[] uuids = getServiceUuids(); + if (uuids.length == 0) { + LOG.warn("no cached services available for " + this); + return false; + } + + for (ParcelUuid uuid : uuids) { + if (uuid != null && aService.equals(uuid.getUuid())) { + return true; + } + } + return false; + } + public String getName() { - String name = null; - if (device != null) { - name = device.getName(); + String deviceName = null; + try { + Method method = device.getClass().getMethod("getAliasName"); + if (method != null) { + deviceName = (String) method.invoke(device); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignore) { + LOG.info("Could not get device alias for " + deviceName); } - if (name == null || name.length() == 0) { - name = GBApplication.getContext().getString(R.string._unknown_); + if (deviceName == null || deviceName.length() == 0) { + deviceName = device.getName(); } - return name; + if (deviceName == null || deviceName.length() == 0) { + deviceName = "(unknown)"; + } + return deviceName; } public short getRssi() { @@ -85,4 +164,9 @@ public class GBDeviceCandidate implements Parcelable { public int hashCode() { return device.getAddress().hashCode() ^ 37; } + + @Override + public String toString() { + return getName() + ": " + getMacAddress(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java index 60ff7b52..8fd47d31 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -12,12 +12,15 @@ 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.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; +//import java.util.UUID; + public class GBDeviceService implements DeviceService { protected final Context mContext; protected final Class mServiceClass; @@ -28,8 +31,7 @@ public class GBDeviceService implements DeviceService { } protected Intent createIntent() { - Intent startIntent = new Intent(mContext, mServiceClass); - return startIntent; + return new Intent(mContext, mServiceClass); } protected void invokeService(Intent intent) { @@ -126,6 +128,14 @@ public class GBDeviceService implements DeviceService { invokeService(intent); } + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + Intent intent = createIntent().setAction(ACTION_SETCANNEDMESSAGES) + .putExtra(EXTRA_CANNEDMESSAGES_TYPE, cannedMessagesSpec.type) + .putExtra(EXTRA_CANNEDMESSAGES, cannedMessagesSpec.cannedMessages); + invokeService(intent); + } + @Override public void onSetMusicState(MusicStateSpec stateSpec) { Intent intent = createIntent().setAction(ACTION_SETMUSICSTATE) @@ -185,6 +195,13 @@ public class GBDeviceService implements DeviceService { invokeService(intent); } + @Override + public void onAppReorder(UUID[] uuids) { + Intent intent = createIntent().setAction(ACTION_APP_REORDER) + .putExtra(EXTRA_APP_UUID, uuids); + invokeService(intent); + } + @Override public void onFetchActivityData() { Intent intent = createIntent().setAction(ACTION_FETCH_ACTIVITY_DATA); @@ -210,6 +227,13 @@ public class GBDeviceService implements DeviceService { invokeService(intent); } + @Override + public void onSetConstantVibration(int intensity) { + Intent intent = createIntent().setAction(ACTION_SET_CONSTANT_VIBRATION) + .putExtra(EXTRA_VIBRATION_INTENSITY, intensity); + invokeService(intent); + } + @Override public void onScreenshotReq() { Intent intent = createIntent().setAction(ACTION_REQUEST_SCREENSHOT); @@ -256,4 +280,17 @@ public class GBDeviceService implements DeviceService { .putExtra(EXTRA_CALENDAREVENT_ID, id); invokeService(intent); } + + @Override + public void onSendConfiguration(String config) { + Intent intent = createIntent().setAction(ACTION_SEND_CONFIGURATION) + .putExtra(EXTRA_CONFIG, config); + invokeService(intent); + } + + @Override + public void onTestNewFunction() { + Intent intent = createIntent().setAction(ACTION_TEST_NEW_FUNCTION); + invokeService(intent); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java index df7bbce3..e292f4b1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityKind.java @@ -5,6 +5,7 @@ import java.util.Arrays; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; public class ActivityKind { + public static final int TYPE_NOT_MEASURED = -1; public static final int TYPE_UNKNOWN = 0; public static final int TYPE_ACTIVITY = 1; public static final int TYPE_LIGHT_SLEEP = 2; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java index 117a8a7a..5d97efec 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySample.java @@ -2,7 +2,26 @@ package nodomain.freeyourgadget.gadgetbridge.model; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -public interface ActivitySample { +/** + * The all-in-one interface for a sample measured by a device with + * one or more sensors. + * + * Each sample is the result of one or more measurements. The values are set for + * a specific point in time (see @{link #getTimestamp()}). + * + * If the sample relates to a user activity (e.g. sleeping, walking, running, ...) + * then the activity is provided through @{link #getKind()}. + * + * Methods will return @{link #NOT_MEASURED} in case no value is available for this + * sample. + * + * The frequency of samples, i.e. the how many samples are recorded per minute, is not specified + * and may vary. + */ +public interface ActivitySample extends TimeStamped { + + int NOT_MEASURED = -1; + /** * Returns the provider of the data. * @@ -10,11 +29,6 @@ public interface ActivitySample { */ SampleProvider getProvider(); - /** - * Timestamp of the sample, resolution is seconds! - */ - int getTimestamp(); - /** * Returns the raw activity kind value as recorded by the SampleProvider */ @@ -42,5 +56,20 @@ public interface ActivitySample { */ int getSteps(); - int getCustomValue(); + /** + * Returns the heart rate measured at the corresponding timestamp. + * The value is returned in heart beats per minute, in the range from + * 0-255, where 255 is an illegal value (e.g. due to a bad measurement) + * + * @return the heart rate value in beats per minute, or -1 if none + */ + int getHeartRate(); + + /** + * Sets the heart rate value of this sample. Typically only used in + * generic db migration. + * + * @param value the value in bpm + */ + void setHeartRate(int value); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java index 358c4a8e..3ed59408 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java @@ -1,8 +1,12 @@ package nodomain.freeyourgadget.gadgetbridge.model; import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; /** @@ -10,50 +14,61 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs; */ public class ActivityUser { - private Integer activityUserGender; - private Integer activityUserYearOfBirth; - private Integer activityUserHeightCm; - private Integer activityUserWeightKg; - private Integer activityUserSleepDuration; + public static final int GENDER_FEMALE = 0; + public static final int GENDER_MALE = 1; + public static final int GENDER_OTHER = 2; - public static final int defaultUserGender = 0; + private String activityUserName; + private int activityUserGender; + private int activityUserYearOfBirth; + private int activityUserHeightCm; + private int activityUserWeightKg; + private int activityUserSleepDuration; + private int activityUserStepsGoal; + + private static final String defaultUserName = "gadgetbridge-user"; + public static final int defaultUserGender = GENDER_FEMALE; public static final int defaultUserYearOfBirth = 0; public static final int defaultUserAge = 0; public static final int defaultUserHeightCm = 175; public static final int defaultUserWeightKg = 70; public static final int defaultUserSleepDuration = 7; + public static final int defaultUserStepsGoal = 8000; + public static final String PREF_USER_NAME = "mi_user_alias"; public static final String PREF_USER_YEAR_OF_BIRTH = "activity_user_year_of_birth"; public static final String PREF_USER_GENDER = "activity_user_gender"; public static final String PREF_USER_HEIGHT_CM = "activity_user_height_cm"; public static final String PREF_USER_WEIGHT_KG = "activity_user_weight_kg"; public static final String PREF_USER_SLEEP_DURATION = "activity_user_sleep_duration"; + public static final String PREF_USER_STEPS_GOAL = MiBandConst.PREF_MIBAND_FITNESS_GOAL; - public int getActivityUserWeightKg() { - if (activityUserWeightKg == null) { - fetchPreferences(); - } + public ActivityUser() { + fetchPreferences(); + } + + public String getName() { + return activityUserName; + } + + public int getWeightKg() { return activityUserWeightKg; } - public int getActivityUserGender() { - if (activityUserGender == null) { - fetchPreferences(); - } + /** + * @see #GENDER_FEMALE + * @see #GENDER_MALE + * @see #GENDER_OTHER + */ + public int getGender() { return activityUserGender; } - public int getActivityUserYearOfBirth() { - if (activityUserYearOfBirth == null) { - fetchPreferences(); - } + public int getYearOfBirth() { return activityUserYearOfBirth; } - public int getActivityUserHeightCm() { - if (activityUserHeightCm == null) { - fetchPreferences(); - } + public int getHeightCm() { return activityUserHeightCm; } @@ -61,18 +76,22 @@ public class ActivityUser { * @return the user defined sleep duration or the default value when none is set or the stored * value is out of any logical bounds. */ - public int getActivityUserSleepDuration() { - if (activityUserSleepDuration == null) { - fetchPreferences(); - } + public int getSleepDuration() { if (activityUserSleepDuration < 1 || activityUserSleepDuration > 24) { activityUserSleepDuration = defaultUserSleepDuration; } return activityUserSleepDuration; } - public int getActivityUserAge() { - int userYear = getActivityUserYearOfBirth(); + public int getStepsGoal() { + if (activityUserStepsGoal < 0) { + activityUserStepsGoal = defaultUserStepsGoal; + } + return activityUserStepsGoal; + } + + public int getAge() { + int userYear = getYearOfBirth(); int age = 25; if (userYear > 1900) { age = Calendar.getInstance().get(Calendar.YEAR) - userYear; @@ -85,10 +104,18 @@ public class ActivityUser { private void fetchPreferences() { Prefs prefs = GBApplication.getPrefs(); + activityUserName = prefs.getString(PREF_USER_NAME, defaultUserName); activityUserGender = prefs.getInt(PREF_USER_GENDER, defaultUserGender); activityUserHeightCm = prefs.getInt(PREF_USER_HEIGHT_CM, defaultUserHeightCm); activityUserWeightKg = prefs.getInt(PREF_USER_WEIGHT_KG, defaultUserWeightKg); activityUserYearOfBirth = prefs.getInt(PREF_USER_YEAR_OF_BIRTH, defaultUserYearOfBirth); activityUserSleepDuration = prefs.getInt(PREF_USER_SLEEP_DURATION, defaultUserSleepDuration); + activityUserStepsGoal = prefs.getInt(PREF_USER_STEPS_GOAL, defaultUserStepsGoal); + } + + public Date getUserBirthday() { + Calendar cal = DateTimeUtils.getCalendarUTC(); + cal.set(GregorianCalendar.YEAR, getYearOfBirth()); + return cal.getTime(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java index 01adde47..1f2e461d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java @@ -26,5 +26,7 @@ public interface Alarm extends Parcelable, Comparable { int getRepetitionMask(); + boolean isRepetitive(); + boolean getRepetition(int dow); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/AppNotificationType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/AppNotificationType.java new file mode 100644 index 00000000..001bb6cf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/AppNotificationType.java @@ -0,0 +1,58 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +import java.util.HashMap; + +public class AppNotificationType extends HashMap { + + private static AppNotificationType _instance; + + public static AppNotificationType getInstance() { + if(_instance == null) { + return (_instance = new AppNotificationType()); + } + + return _instance; + } + + private AppNotificationType() { + // Generic Email + put("com.fsck.k9", NotificationType.GENERIC_EMAIL); + put("com.android.email", NotificationType.GENERIC_EMAIL); + + // Generic SMS + put("com.moez.QKSMS", NotificationType.GENERIC_SMS); + put("com.android.mms", NotificationType.GENERIC_SMS); + put("com.android.messaging", NotificationType.GENERIC_SMS); + put("com.sonyericsson.conversations", NotificationType.GENERIC_SMS); + put("org.smssecure.smssecure", NotificationType.GENERIC_SMS); + + // Conversations + put("eu.siacs.conversations", NotificationType.CONVERSATIONS); + + // Signal + put("org.thoughtcrime.securesms", NotificationType.SIGNAL); + + // Telegram + put("org.telegram.messenger", NotificationType.TELEGRAM); + + // Twitter + put("org.mariotaku.twidere", NotificationType.TWITTER); + put("com.twitter.android", NotificationType.TWITTER); + put("org.andstatus.app", NotificationType.TWITTER); + put("org.mustard.android", NotificationType.TWITTER); + + // Facebook + put("me.zeeroooo.materialfb", NotificationType.FACEBOOK); + put("it.rignanese.leo.slimfacebook", NotificationType.FACEBOOK); + put("me.jakelane.wrapperforfacebook", NotificationType.FACEBOOK); + put("com.facebook.katana", NotificationType.FACEBOOK); + put("org.indywidualni.fblite", NotificationType.FACEBOOK); + + // Facebook Messenger + put("com.facebook.orca", NotificationType.FACEBOOK_MESSENGER); + + // WhatsApp + put("com.whatsapp", NotificationType.WHATSAPP); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CannedMessagesSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CannedMessagesSpec.java new file mode 100644 index 00000000..5975cf7b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/CannedMessagesSpec.java @@ -0,0 +1,10 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +public class CannedMessagesSpec { + public static final byte TYPE_GENERIC = 0; + public static final byte TYPE_MISSEDCALLS = 1; + public static final byte TYPE_NEWSMS = 2; + + public int type; + public String[] cannedMessages; +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index b3921b1b..82fd3958 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -12,10 +12,12 @@ import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; public interface DeviceService extends EventHandler { String PREFIX = "nodomain.freeyourgadget.gadgetbridge.devices"; + String ACTION_MIBAND2_AUTH = PREFIX + ".action.miban2_auth"; String ACTION_START = PREFIX + ".action.start"; String ACTION_CONNECT = PREFIX + ".action.connect"; String ACTION_NOTIFICATION = PREFIX + ".action.notification"; String ACTION_CALLSTATE = PREFIX + ".action.callstate"; + String ACTION_SETCANNEDMESSAGES = PREFIX + ".action.setcannedmessages"; String ACTION_SETTIME = PREFIX + ".action.settime"; String ACTION_SETMUSICINFO = PREFIX + ".action.setmusicinfo"; String ACTION_SETMUSICSTATE = PREFIX + ".action.setmusicstate"; @@ -25,20 +27,29 @@ public interface DeviceService extends EventHandler { String ACTION_STARTAPP = PREFIX + ".action.startapp"; String ACTION_DELETEAPP = PREFIX + ".action.deleteapp"; String ACTION_APP_CONFIGURE = PREFIX + ".action.app_configure"; + String ACTION_APP_REORDER = PREFIX + ".action.app_reorder"; String ACTION_INSTALL = PREFIX + ".action.install"; String ACTION_REBOOT = PREFIX + ".action.reboot"; String ACTION_HEARTRATE_TEST = PREFIX + ".action.heartrate_test"; String ACTION_FETCH_ACTIVITY_DATA = PREFIX + ".action.fetch_activity_data"; String ACTION_DISCONNECT = PREFIX + ".action.disconnect"; String ACTION_FIND_DEVICE = PREFIX + ".action.find_device"; + String ACTION_SET_CONSTANT_VIBRATION = PREFIX + ".action.set_constant_vibration"; String ACTION_SET_ALARMS = PREFIX + ".action.set_alarms"; String ACTION_ENABLE_REALTIME_STEPS = PREFIX + ".action.enable_realtime_steps"; + String ACTION_REALTIME_SAMPLES = PREFIX + ".action.realtime_samples"; + /** + * Use EXTRA_REALTIME_SAMPLE instead + */ + @Deprecated String ACTION_REALTIME_STEPS = PREFIX + ".action.realtime_steps"; String ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT = PREFIX + ".action.realtime_hr_measurement"; String ACTION_ENABLE_HEARTRATE_SLEEP_SUPPORT = PREFIX + ".action.enable_heartrate_sleep_support"; String ACTION_HEARTRATE_MEASUREMENT = PREFIX + ".action.hr_measurement"; String ACTION_ADD_CALENDAREVENT = PREFIX + ".action.add_calendarevent"; String ACTION_DELETE_CALENDAREVENT = PREFIX + ".action.delete_calendarevent"; + String ACTION_SEND_CONFIGURATION = PREFIX + ".action.send_configuration"; + String ACTION_TEST_NEW_FUNCTION = PREFIX + ".action.test_new_function"; String EXTRA_DEVICE_ADDRESS = "device_address"; String EXTRA_NOTIFICATION_BODY = "notification_body"; String EXTRA_NOTIFICATION_FLAGS = "notification_flags"; @@ -50,8 +61,11 @@ public interface DeviceService extends EventHandler { String EXTRA_NOTIFICATION_TITLE = "notification_title"; String EXTRA_NOTIFICATION_TYPE = "notification_type"; String EXTRA_FIND_START = "find_start"; + String EXTRA_VIBRATION_INTENSITY = "vibration_intensity"; String EXTRA_CALL_COMMAND = "call_command"; String EXTRA_CALL_PHONENUMBER = "call_phonenumber"; + String EXTRA_CANNEDMESSAGES = "cannedmessages"; + String EXTRA_CANNEDMESSAGES_TYPE = "cannedmessages_type"; String EXTRA_MUSIC_ARTIST = "music_artist"; String EXTRA_MUSIC_ALBUM = "music_album"; String EXTRA_MUSIC_TRACK = "music_track"; @@ -67,11 +81,21 @@ public interface DeviceService extends EventHandler { String EXTRA_APP_START = "app_start"; String EXTRA_APP_CONFIG = "app_config"; String EXTRA_URI = "uri"; + String EXTRA_CONFIG = "config"; String EXTRA_ALARMS = "alarms"; String EXTRA_PERFORM_PAIR = "perform_pair"; String EXTRA_BOOLEAN_ENABLE = "enable_realtime_steps"; + /** + * Use EXTRA_REALTIME_SAMPLE instead + */ + @Deprecated String EXTRA_REALTIME_STEPS = "realtime_steps"; + String EXTRA_REALTIME_SAMPLE = "realtime_sample"; String EXTRA_TIMESTAMP = "timestamp"; + /** + * Use EXTRA_REALTIME_SAMPLE instead + */ + @Deprecated String EXTRA_HEART_RATE_VALUE = "hr_value"; String EXTRA_CALENDAREVENT_ID = "calendarevent_id"; String EXTRA_CALENDAREVENT_TYPE = "calendarevent_type"; @@ -79,6 +103,7 @@ public interface DeviceService extends EventHandler { String EXTRA_CALENDAREVENT_DURATION = "calendarevent_duration"; String EXTRA_CALENDAREVENT_TITLE = "calendarevent_title"; String EXTRA_CALENDAREVENT_DESCRIPTION = "calendarevent_description"; + String EXTRA_MIBAND2_AUTH_BYTE = "miband2_auth_byte"; void start(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index e7b357af..5c9d2663 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -2,10 +2,39 @@ package nodomain.freeyourgadget.gadgetbridge.model; /** * For every supported device, a device type constant must exist. + * + * Note: they key of every constant is stored in the DB, so it is fixed forever, + * and may not be changed. */ public enum DeviceType { - UNKNOWN, - PEBBLE, - TEST, - MIBAND + UNKNOWN(-1), + PEBBLE(1), + MIBAND(10), + MIBAND2(11), + VIBRATISSIMO(20), + LIVEVIEW(30), + TEST(1000); + + private final int key; + + DeviceType(int key) { + this.key = key; + } + + public int getKey() { + return key; + } + + public boolean isSupported() { + return this != UNKNOWN; + } + + public static DeviceType fromKey(int key) { + for (DeviceType type : values()) { + if (type.key == key) { + return type; + } + } + return DeviceType.UNKNOWN; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java index af52f02f..aa9f67ca 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java @@ -1,5 +1,7 @@ package nodomain.freeyourgadget.gadgetbridge.model; +import java.util.Objects; + public class MusicSpec { public static final int MUSIC_UNDEFINED = 0; public static final int MUSIC_PLAY = 1; @@ -14,4 +16,23 @@ public class MusicSpec { public int duration; public int trackCount; public int trackNr; + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof MusicSpec)) { + return false; + } + MusicSpec musicSpec = (MusicSpec) obj; + + return Objects.equals(this.artist, musicSpec.artist) && + Objects.equals(this.album, musicSpec.album) && + Objects.equals(this.track, musicSpec.track) && + this.duration == musicSpec.duration && + this.trackCount == musicSpec.trackCount && + this.trackNr == musicSpec.trackNr; + + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java index ee5510fb..fb15948d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java @@ -14,4 +14,21 @@ public class MusicStateSpec { public int playRate; public byte shuffle; public byte repeat; + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof MusicStateSpec)) { + return false; + } + MusicStateSpec stateSpec = (MusicStateSpec) obj; + + return this.state == stateSpec.state && + Math.abs(this.position - stateSpec.position)<=2 && + this.playRate == stateSpec.playRate && + this.shuffle == stateSpec.shuffle && + this.repeat == stateSpec.repeat; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java index 63feca89..e3e32070 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java @@ -1,12 +1,58 @@ package nodomain.freeyourgadget.gadgetbridge.model; +import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleColor; +import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleIconID; + public enum NotificationType { - UNDEFINED, + UNKNOWN(PebbleIconID.NOTIFICATION_GENERIC, PebbleColor.Red), - CHAT, - EMAIL, - FACEBOOK, - SMS, - TWITTER, + CONVERSATIONS(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.Inchworm), + GENERIC_EMAIL(PebbleIconID.GENERIC_EMAIL, PebbleColor.JaegerGreen), + GENERIC_NAVIGATION(PebbleIconID.LOCATION, PebbleColor.Orange), + GENERIC_SMS(PebbleIconID.GENERIC_SMS, PebbleColor.VividViolet), + FACEBOOK(PebbleIconID.NOTIFICATION_FACEBOOK, PebbleColor.Liberty), + FACEBOOK_MESSENGER(PebbleIconID.NOTIFICATION_FACEBOOK_MESSENGER, PebbleColor.VeryLightBlue), + SIGNAL(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.BlueMoon), + TWITTER(PebbleIconID.NOTIFICATION_TWITTER, PebbleColor.BlueMoon), + TELEGRAM(PebbleIconID.NOTIFICATION_TELEGRAM, PebbleColor.PictonBlue), + WHATSAPP(PebbleIconID.NOTIFICATION_WHATSAPP, PebbleColor.MayGreen); + + public int icon; + public byte color; + + NotificationType(int icon, byte color) { + this.icon = icon; + this.color = color; + } + + /** + * Returns the enum constant as a fixed String value, e.g. to be used + * as preference key. In case the keys are ever changed, this method + * may be used to bring backward compatibility. + */ + public String getFixedValue() { + return name().toLowerCase(); + } + + public String getGenericType() { + switch (this) { + case GENERIC_EMAIL: + case GENERIC_NAVIGATION: + case GENERIC_SMS: + return getFixedValue(); + case FACEBOOK: + case TWITTER: + return "generic_social"; + case CONVERSATIONS: + case FACEBOOK_MESSENGER: + case SIGNAL: + case TELEGRAM: + case WHATSAPP: + return "generic_chat"; + case UNKNOWN: + default: + return "generic"; + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TimeStamped.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TimeStamped.java new file mode 100644 index 00000000..b295f798 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/TimeStamped.java @@ -0,0 +1,8 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +public interface TimeStamped { + /** + * Unix timestamp of the sample, i.e. the number of seconds since 1970-01-01 00:00:00 UTC. + */ + int getTimestamp(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ValidByDate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ValidByDate.java new file mode 100644 index 00000000..2c634d6e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ValidByDate.java @@ -0,0 +1,8 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +import java.util.Date; + +public interface ValidByDate { + Date getValidFromUTC(); + Date getValidToUTC(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java index 21d7965b..ef37a75b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -24,7 +24,7 @@ import java.util.Objects; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; -import nodomain.freeyourgadget.gadgetbridge.activities.AppManagerActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsHost; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; @@ -61,6 +61,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { private Context context; private boolean autoReconnect; + @Override public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) { this.gbDevice = gbDevice; this.btAdapter = btAdapter; @@ -152,7 +153,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { return; } gbDevice.setFirmwareVersion(infoEvent.fwVersion); - gbDevice.setHardwareVersion(infoEvent.hwVersion); + gbDevice.setModel(infoEvent.hwVersion); gbDevice.sendDeviceUpdateIntent(context); } @@ -160,7 +161,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { Context context = getContext(); LOG.info("Got event for APP_INFO"); - Intent appInfoIntent = new Intent(AppManagerActivity.ACTION_REFRESH_APPLIST); + Intent appInfoIntent = new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST); int appCount = appInfoEvent.apps.length; appInfoIntent.putExtra("app_count", appCount); for (Integer i = 0; i < appCount; i++) { @@ -243,10 +244,12 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { action = NotificationListener.ACTION_MUTE; break; case REPLY: - String phoneNumber = (String) GBApplication.getIDSenderLookup().lookup(deviceEvent.handle); - if (phoneNumber != null) { - LOG.info("got notfication reply for SMS from " + phoneNumber + " : " + deviceEvent.reply); - SmsManager.getDefault().sendTextMessage(phoneNumber, null, deviceEvent.reply, null, null); + if (deviceEvent.phoneNumber == null) { + deviceEvent.phoneNumber = (String) GBApplication.getIDSenderLookup().lookup(deviceEvent.handle); + } + if (deviceEvent.phoneNumber != null) { + LOG.info("got notfication reply for SMS from " + deviceEvent.phoneNumber + " : " + deviceEvent.reply); + SmsManager.getDefault().sendTextMessage(deviceEvent.phoneNumber, null, deviceEvent.reply, null, null); } else { LOG.info("got notfication reply for notification id " + deviceEvent.handle + " : " + deviceEvent.reply); action = NotificationListener.ACTION_REPLY; @@ -279,11 +282,11 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { (BatteryState.BATTERY_LOW.equals(deviceEvent.state) || BatteryState.BATTERY_NORMAL.equals(deviceEvent.state)) ) { - GB.updateBatteryNotification(context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), deviceEvent.level), + GB.updateBatteryNotification(context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), String.valueOf(deviceEvent.level)), deviceEvent.extendedInfoAvailable() ? - context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), deviceEvent.level) + "\n" + + context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), String.valueOf(deviceEvent.level)) + "\n" + context.getString(R.string.notif_battery_low_bigtext_last_charge_time, DateFormat.getDateTimeInstance().format(deviceEvent.lastChargeTime.getTime())) + - context.getString(R.string.notif_battery_low_bigtext_number_of_charges, deviceEvent.numCharges) + context.getString(R.string.notif_battery_low_bigtext_number_of_charges, String.valueOf(deviceEvent.numCharges)) : "" , context); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index 7903ff22..8c25a793 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -25,6 +25,10 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.OnboardingActivity; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.K9Receiver; @@ -37,6 +41,8 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; 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.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; @@ -48,6 +54,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ADD_CALENDAREVENT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_APP_CONFIGURE; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_APP_REORDER; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CALLSTATE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_DELETEAPP; @@ -65,12 +72,16 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_RE import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_APPINFO; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_DEVICEINFO; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_SCREENSHOT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SEND_CONFIGURATION; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETCANNEDMESSAGES; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICINFO; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICSTATE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETTIME; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_ALARMS; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_CONSTANT_VIBRATION; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_TEST_NEW_FUNCTION; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_ALARMS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_CONFIG; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_APP_START; @@ -84,6 +95,9 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CAL import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_TYPE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_COMMAND; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_PHONENUMBER; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES_TYPE; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONFIG; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_DEVICE_ADDRESS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM; @@ -108,9 +122,11 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOT import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_TYPE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_PERFORM_PAIR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_URI; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_VIBRATION_INTENSITY; public class DeviceCommunicationService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener { private static final Logger LOG = LoggerFactory.getLogger(DeviceCommunicationService.class); + private static DeviceSupportFactory DEVICE_SUPPORT_FACTORY = null; private boolean mStarted = false; @@ -129,6 +145,19 @@ public class DeviceCommunicationService extends Service implements SharedPrefere private Random mRandom = new Random(); + /** + * For testing! + * + * @param factory + */ + public static void setDeviceSupportFactory(DeviceSupportFactory factory) { + DEVICE_SUPPORT_FACTORY = factory; + } + + public DeviceCommunicationService() { + + } + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -140,6 +169,27 @@ public class DeviceCommunicationService extends Service implements SharedPrefere boolean enableReceivers = mDeviceSupport != null && (mDeviceSupport.useAutoConnect() || mGBDevice.isInitialized()); setReceiversEnableState(enableReceivers); GB.updateNotification(mGBDevice.getName() + " " + mGBDevice.getStateString(), mGBDevice.isInitialized(), context); + + if (device.isInitialized()) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + DaoSession session = dbHandler.getDaoSession(); + boolean askForDBMigration = false; + if (DBHelper.findDevice(device, session) == null && device.getType() != DeviceType.VIBRATISSIMO && (device.getType() != DeviceType.LIVEVIEW)) { + askForDBMigration = true; + } + DBHelper.getDevice(device, session); // implicitly creates the device in database if not present, and updates device attributes + if (askForDBMigration) { + DBHelper dbHelper = new DBHelper(context); + if (dbHelper.getOldActivityDatabaseHandler() != null) { + Intent startIntent = new Intent(context, OnboardingActivity.class); + startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startIntent.putExtra(GBDevice.EXTRA_DEVICE, device); + startActivity(startIntent); + } + } + } catch (Exception ignore) { + } + } } else { LOG.error("Got ACTION_DEVICE_CHANGED from unexpected device: " + mGBDevice); } @@ -152,13 +202,20 @@ public class DeviceCommunicationService extends Service implements SharedPrefere LOG.debug("DeviceCommunicationService is being created"); super.onCreate(); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED)); - mFactory = new DeviceSupportFactory(this); + mFactory = getDeviceSupportFactory(); if (hasPrefs()) { getPrefs().getPreferences().registerOnSharedPreferenceChangeListener(this); } } + private DeviceSupportFactory getDeviceSupportFactory() { + if (DEVICE_SUPPORT_FACTORY != null) { + return DEVICE_SUPPORT_FACTORY; + } + return new DeviceSupportFactory(this); + } + @Override public synchronized int onStartCommand(Intent intent, int flags, int startId) { @@ -261,14 +318,14 @@ public class DeviceCommunicationService extends Service implements SharedPrefere notificationSpec.id = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1); notificationSpec.flags = intent.getIntExtra(EXTRA_NOTIFICATION_FLAGS, 0); notificationSpec.sourceName = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCENAME); - if (notificationSpec.type == NotificationType.SMS && notificationSpec.phoneNumber != null) { + if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) { notificationSpec.sender = getContactDisplayNameByNumber(notificationSpec.phoneNumber); notificationSpec.id = mRandom.nextInt(); // FIXME: add this in external SMS Receiver? GBApplication.getIDSenderLookup().add(notificationSpec.id, notificationSpec.phoneNumber); } if (((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_REPLY) > 0) - || (notificationSpec.type == NotificationType.SMS && notificationSpec.phoneNumber != null)) { + || (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null)) { // NOTE: maybe not where it belongs if (prefs.getBoolean("pebble_force_untested", false)) { // I would rather like to save that as an array in ShadredPreferences @@ -330,6 +387,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere mDeviceSupport.onFindDevice(start); break; } + case ACTION_SET_CONSTANT_VIBRATION: { + int intensity = intent.getIntExtra(EXTRA_VIBRATION_INTENSITY, 0); + mDeviceSupport.onSetConstantVibration(intensity); + break; + } case ACTION_CALLSTATE: int command = intent.getIntExtra(EXTRA_CALL_COMMAND, CallSpec.CALL_UNDEFINED); @@ -345,6 +407,15 @@ public class DeviceCommunicationService extends Service implements SharedPrefere callSpec.name = callerName; mDeviceSupport.onSetCallState(callSpec); break; + case ACTION_SETCANNEDMESSAGES: + int type = intent.getIntExtra(EXTRA_CANNEDMESSAGES_TYPE, -1); + String[] cannedMessages = intent.getStringArrayExtra(EXTRA_CANNEDMESSAGES); + + CannedMessagesSpec cannedMessagesSpec = new CannedMessagesSpec(); + cannedMessagesSpec.type = type; + cannedMessagesSpec.cannedMessages = cannedMessages; + mDeviceSupport.onSetCannedMessages(cannedMessagesSpec); + break; case ACTION_SETTIME: mDeviceSupport.onSetTime(); break; @@ -360,11 +431,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere break; case ACTION_SETMUSICSTATE: MusicStateSpec stateSpec = new MusicStateSpec(); - stateSpec.shuffle = intent.getByteExtra(EXTRA_MUSIC_SHUFFLE, (byte)0); - stateSpec.repeat = intent.getByteExtra(EXTRA_MUSIC_REPEAT, (byte)0); + stateSpec.shuffle = intent.getByteExtra(EXTRA_MUSIC_SHUFFLE, (byte) 0); + stateSpec.repeat = intent.getByteExtra(EXTRA_MUSIC_REPEAT, (byte) 0); stateSpec.position = intent.getIntExtra(EXTRA_MUSIC_POSITION, 0); stateSpec.playRate = intent.getIntExtra(EXTRA_MUSIC_RATE, 0); - stateSpec.state = intent.getByteExtra(EXTRA_MUSIC_STATE, (byte)0); + stateSpec.state = intent.getByteExtra(EXTRA_MUSIC_STATE, (byte) 0); mDeviceSupport.onSetMusicState(stateSpec); break; case ACTION_REQUEST_APPINFO: @@ -388,6 +459,12 @@ public class DeviceCommunicationService extends Service implements SharedPrefere UUID uuid = (UUID) intent.getSerializableExtra(EXTRA_APP_UUID); String config = intent.getStringExtra(EXTRA_APP_CONFIG); mDeviceSupport.onAppConfiguration(uuid, config); + break; + } + case ACTION_APP_REORDER: { + UUID[] uuids = (UUID[]) intent.getSerializableExtra(EXTRA_APP_UUID); + mDeviceSupport.onAppReorder(uuids); + break; } case ACTION_INSTALL: Uri uri = intent.getParcelableExtra(EXTRA_URI); @@ -415,20 +492,20 @@ public class DeviceCommunicationService extends Service implements SharedPrefere mDeviceSupport.onEnableRealtimeHeartRateMeasurement(enable); break; } + case ACTION_SEND_CONFIGURATION: { + String config = intent.getStringExtra(EXTRA_CONFIG); + mDeviceSupport.onSendConfiguration(config); + break; + } + case ACTION_TEST_NEW_FUNCTION: { + mDeviceSupport.onTestNewFunction(); + break; + } } return START_STICKY; } - /** - * For testing! - * - * @param factory - */ - public void setDeviceSupportFactory(DeviceSupportFactory factory) { - mFactory = factory; - } - /** * Disposes the current DeviceSupport instance (if any) and sets a new device support instance * (if not null). @@ -567,7 +644,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere setDeviceSupport(null); NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.cancel(GB.NOTIFICATION_ID); // need to do this because the updated notification wont be cancelled when service stops + nm.cancel(GB.NOTIFICATION_ID); // need to do this because the updated notification won't be cancelled when service stops } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index 6bc07789..7b309818 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -10,8 +10,11 @@ import java.util.EnumSet; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class DeviceSupportFactory { @@ -84,13 +87,22 @@ public class DeviceSupportFactory { case MIBAND: deviceSupport = new ServiceDeviceSupport(new MiBandSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case MIBAND2: + deviceSupport = new ServiceDeviceSupport(new MiBand2Support(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; + case VIBRATISSIMO: + deviceSupport = new ServiceDeviceSupport(new VibratissimoSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; + case LIVEVIEW: + deviceSupport = new ServiceDeviceSupport(new LiveviewSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; } if (deviceSupport != null) { deviceSupport.setContext(gbDevice, mBtAdapter, mContext); return deviceSupport; } } catch (Exception e) { - throw new GBException(mContext.getString(R.string.cannot_connect_bt_address_invalid_, e)); + throw new GBException(mContext.getString(R.string.cannot_connect_bt_address_invalid_), e); } } return null; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index e284f83d..a90ee63c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -15,6 +15,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; 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; @@ -151,6 +152,14 @@ public class ServiceDeviceSupport implements DeviceSupport { delegate.onSetCallState(callSpec); } + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + if (checkBusy("set canned messages")) { + return; + } + delegate.onSetCannedMessages(cannedMessagesSpec); + } + @Override public void onSetMusicState(MusicStateSpec stateSpec) { if (checkBusy("set music state")) { @@ -207,6 +216,14 @@ public class ServiceDeviceSupport implements DeviceSupport { delegate.onAppConfiguration(uuid, config); } + @Override + public void onAppReorder(UUID[] uuids) { + if (checkBusy("app reorder")) { + return; + } + delegate.onAppReorder(uuids); + } + @Override public void onFetchActivityData() { if (checkBusy("fetch activity data")) { @@ -239,6 +256,14 @@ public class ServiceDeviceSupport implements DeviceSupport { delegate.onFindDevice(start); } + @Override + public void onSetConstantVibration(int intensity) { + if (checkBusy("set constant vibration")) { + return; + } + delegate.onSetConstantVibration(intensity); + } + @Override public void onScreenshotReq() { if (checkBusy("request screenshot")) { @@ -294,4 +319,20 @@ public class ServiceDeviceSupport implements DeviceSupport { } delegate.onDeleteCalendarEvent(type, id); } + + @Override + public void onSendConfiguration(String config) { + if (checkBusy("send configuration: " + config)) { + return; + } + delegate.onSendConfiguration(config); + } + + @Override + public void onTestNewFunction() { + if (checkBusy("test new function event")) { + return; + } + delegate.onTestNewFunction(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index 26118112..e87c50e3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -6,17 +6,19 @@ import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.CheckInitializedAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; /** * Abstract base class for all devices connected through Bluetooth Low Energy (LE) aka @@ -30,14 +32,21 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.CheckInitialize * @see BtLEQueue */ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback { - private static final Logger LOG = LoggerFactory.getLogger(AbstractBTLEDeviceSupport.class); - private BtLEQueue mQueue; private HashMap mAvailableCharacteristics; private final Set mSupportedServices = new HashSet<>(4); + private Logger logger; + private final List> mSupportedProfiles = new ArrayList<>(); public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb"; //this is common for all BTLE devices. see http://stackoverflow.com/questions/18699251/finding-out-android-bluetooth-le-gatt-profiles + public AbstractBTLEDeviceSupport(Logger logger) { + this.logger = logger; + if (logger == null) { + throw new IllegalArgumentException("logger must not be null"); + } + } + @Override public boolean connect() { if (mQueue == null) { @@ -73,7 +82,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im } } - protected TransactionBuilder createTransactionBuilder(String taskName) { + public TransactionBuilder createTransactionBuilder(String taskName) { return new TransactionBuilder(taskName); } @@ -107,7 +116,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im * @throws IOException * @see {@link #performInitialized(String)} */ - protected void performConnected(Transaction transaction) throws IOException { + public void performConnected(Transaction transaction) throws IOException { if (!isConnected()) { if (!connect()) { throw new IOException("2: Unable to connect to device: " + getDevice()); @@ -116,6 +125,19 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im getQueue().add(transaction); } + /** + * Performs the actions of the given transaction as soon as possible, + * that is, before any other queued transactions, but after the actions + * of the currently executing transaction. + * @param builder + */ + public void performImmediately(TransactionBuilder builder) throws IOException { + if (!isConnected()) { + throw new IOException("Not connected to device: " + getDevice()); + } + getQueue().insert(builder.getTransaction()); + } + public BtLEQueue getQueue() { return mQueue; } @@ -131,6 +153,10 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im mSupportedServices.add(aSupportedService); } + protected void addSupportedProfile(AbstractBleProfile profile) { + mSupportedProfiles.add(profile); + } + /** * Returns the characteristic matching the given UUID. Only characteristics * are returned whose service is marked as supported. @@ -148,22 +174,27 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im private void gattServicesDiscovered(List discoveredGattServices) { if (discoveredGattServices == null) { + logger.warn("No gatt services discovered: null!"); return; } Set supportedServices = getSupportedServices(); mAvailableCharacteristics = new HashMap<>(); for (BluetoothGattService service : discoveredGattServices) { if (supportedServices.contains(service.getUuid())) { + logger.debug("discovered supported service: " + BleNamesResolver.resolveServiceName(service.getUuid().toString()) + ": " + service.getUuid()); List characteristics = service.getCharacteristics(); if (characteristics == null || characteristics.isEmpty()) { - LOG.warn("Supported LE service " + service.getUuid() + "did not return any characteristics"); + logger.warn("Supported LE service " + service.getUuid() + "did not return any characteristics"); continue; } HashMap intmAvailableCharacteristics = new HashMap<>(characteristics.size()); for (BluetoothGattCharacteristic characteristic : characteristics) { intmAvailableCharacteristics.put(characteristic.getUuid(), characteristic); + logger.info(" characteristic: " + BleNamesResolver.resolveCharacteristicName(characteristic.getUuid().toString()) + ": " + characteristic.getUuid()); } mAvailableCharacteristics.putAll(intmAvailableCharacteristics); + } else { + logger.debug("discovered unsupported service: " + BleNamesResolver.resolveServiceName(service.getUuid().toString()) + ": " + service.getUuid()); } } } @@ -172,9 +203,22 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im return mSupportedServices; } + /** + * Utility method that may be used to log incoming messages when we don't know how to deal with them yet. + * + * @param value + */ + public void logMessageContent(byte[] value) { + logger.info("RECEIVED DATA WITH LENGTH: " + ((value != null) ? value.length : "(null)")); + Logging.logBytes(logger, value); + } + // default implementations of event handler methods (gatt callbacks) @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + for (AbstractBleProfile profile : mSupportedProfiles) { + profile.onConnectionStateChange(gatt, status, newState); + } } @Override @@ -184,29 +228,62 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im } @Override - public void onCharacteristicRead(BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic, int status) { + public boolean onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + for (AbstractBleProfile profile : mSupportedProfiles) { + if (profile.onCharacteristicRead(gatt, characteristic, status)) { + return true; + } + } + return false; } @Override - public void onCharacteristicWrite(BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic, int status) { + public boolean onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + for (AbstractBleProfile profile : mSupportedProfiles) { + if (profile.onCharacteristicWrite(gatt, characteristic, status)) { + return true; + } + } + return false; } @Override - public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + public boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + for (AbstractBleProfile profile : mSupportedProfiles) { + if (profile.onDescriptorRead(gatt, descriptor, status)) { + return true; + } + } + return false; } @Override - public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + for (AbstractBleProfile profile : mSupportedProfiles) { + if (profile.onDescriptorWrite(gatt, descriptor, status)) { + return true; + } + } + return false; } @Override - public void onCharacteristicChanged(BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic) { + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + for (AbstractBleProfile profile : mSupportedProfiles) { + if (profile.onCharacteristicChanged(gatt, characteristic)) { + return true; + } + } + return false; } @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + for (AbstractBleProfile profile : mSupportedProfiles) { + profile.onReadRemoteRssi(gatt, rssi, status); + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java index 258199ad..dc8da3f8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java @@ -59,7 +59,7 @@ public abstract class AbstractBTLEOperation * Subclasses must implement this. When invoked, #prePerform() returned * successfully. * Note that subclasses HAVE TO call #operationFinished() when the entire - * opreation is done (successful or not). + * operation is done (successful or not). * * @throws IOException */ @@ -67,7 +67,7 @@ public abstract class AbstractBTLEOperation /** * You MUST call this method when the operation has finished, either - * successfull or unsuccessfully. + * successfully or unsuccessfully. * * @throws IOException */ @@ -105,8 +105,10 @@ public abstract class AbstractBTLEOperation } protected void unsetBusy() { - getDevice().unsetBusyTask(); - getDevice().sendDeviceUpdateIntent(getContext()); + if (getDevice().isBusy()) { + getDevice().unsetBusyTask(); + getDevice().sendDeviceUpdateIntent(getContext()); + } } public boolean isOperationRunning() { @@ -133,28 +135,28 @@ public abstract class AbstractBTLEOperation } @Override - public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - mSupport.onCharacteristicRead(gatt, characteristic, status); + public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + return mSupport.onCharacteristicRead(gatt, characteristic, status); } @Override - public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - mSupport.onCharacteristicWrite(gatt, characteristic, status); + public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + return mSupport.onCharacteristicWrite(gatt, characteristic, status); } @Override - public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - mSupport.onCharacteristicChanged(gatt, characteristic); + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + return mSupport.onCharacteristicChanged(gatt, characteristic); } @Override - public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - mSupport.onDescriptorRead(gatt, descriptor, status); + public boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + return mSupport.onDescriptorRead(gatt, descriptor, status); } @Override - public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - mSupport.onDescriptorWrite(gatt, descriptor, status); + public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + return mSupport.onDescriptorWrite(gatt, descriptor, status); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractGattCallback.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractGattCallback.java index 76f0a839..5ef65460 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractGattCallback.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractGattCallback.java @@ -17,23 +17,28 @@ public abstract class AbstractGattCallback implements GattCallback { } @Override - public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + return false; } @Override - public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + return false; } @Override - public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + return false; } @Override - public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + public boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + return false; } @Override - public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + return false; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java new file mode 100644 index 00000000..1bb4b88f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java @@ -0,0 +1,218 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; + +/** + * Provides methods to convert standard BLE units to byte sequences and vice versa. + */ +public class BLETypeConversions { + /** + * Converts a timestamp to the byte sequence to be sent to the current time characteristic + * + * @param timestamp + * @return + * @see GattCharacteristic#UUID_CHARACTERISTIC_CURRENT_TIME + */ + public static byte[] calendarToRawBytes(Calendar timestamp, boolean honorDeviceTimeOffset) { + + // The mi-band device currently records sleep + // only if it happens after 10pm and before 7am. + // The offset is used to trick the device to record sleep + // in non-standard hours. + // If you usually sleep, say, from 6am to 2pm, set the + // shift to -8, so at 6am the device thinks it's still 10pm + // of the day before. + if (honorDeviceTimeOffset) { + int offsetInHours = MiBandCoordinator.getDeviceTimeOffsetHours(); + if (offsetInHours != 0) { + timestamp.add(Calendar.HOUR_OF_DAY, offsetInHours); + } + } + + // MiBand2: + // year,year,month,dayofmonth,hour,minute,second,dayofweek,0,0,tz + + byte[] year = fromUint16(timestamp.get(Calendar.YEAR)); + return new byte[] { + year[0], + year[1], + fromUint8(timestamp.get(Calendar.MONTH) + 1), + fromUint8(timestamp.get(Calendar.DATE)), + fromUint8(timestamp.get(Calendar.HOUR_OF_DAY)), + fromUint8(timestamp.get(Calendar.MINUTE)), + fromUint8(timestamp.get(Calendar.SECOND)), + dayOfWeekToRawBytes(timestamp), + 0, // fractions256 (not set) + // 0 (DST offset?) Mi2 + // k (tz) Mi2 + }; + } + + /** + * Similar to calendarToRawBytes, but only up to (and including) the MINUTES. + * @param timestamp + * @param honorDeviceTimeOffset + * @return + */ + public static byte[] shortCalendarToRawBytes(Calendar timestamp, boolean honorDeviceTimeOffset) { + + // The mi-band device currently records sleep + // only if it happens after 10pm and before 7am. + // The offset is used to trick the device to record sleep + // in non-standard hours. + // If you usually sleep, say, from 6am to 2pm, set the + // shift to -8, so at 6am the device thinks it's still 10pm + // of the day before. + if (honorDeviceTimeOffset) { + int offsetInHours = MiBandCoordinator.getDeviceTimeOffsetHours(); + if (offsetInHours != 0) { + timestamp.add(Calendar.HOUR_OF_DAY, offsetInHours); + } + } + + // MiBand2: + // year,year,month,dayofmonth,hour,minute + + byte[] year = fromUint16(timestamp.get(Calendar.YEAR)); + return new byte[] { + year[0], + year[1], + fromUint8(timestamp.get(Calendar.MONTH) + 1), + fromUint8(timestamp.get(Calendar.DATE)), + fromUint8(timestamp.get(Calendar.HOUR_OF_DAY)), + fromUint8(timestamp.get(Calendar.MINUTE)) + }; + } + + private static int getMiBand2TimeZone(int rawOffset) { + int offsetMinutes = rawOffset / 1000 / 60; + rawOffset = offsetMinutes < 0 ? -1 : 1; + offsetMinutes = Math.abs(offsetMinutes); + int offsetHours = offsetMinutes / 60; + rawOffset *= offsetMinutes % 60 / 15 + offsetHours * 4; + return rawOffset; + } + + private static byte dayOfWeekToRawBytes(Calendar cal) { + int calValue = cal.get(Calendar.DAY_OF_WEEK); + switch (calValue) { + case Calendar.SUNDAY: + return 7; + default: + return (byte) (calValue - 1); + } + } + + /** + * uses the standard algorithm to convert bytes received from the MiBand to a Calendar object + * + * @param value + * @return + */ + public static GregorianCalendar rawBytesToCalendar(byte[] value, boolean honorDeviceTimeOffset) { + if (value.length >= 7) { + int year = toUint16(value[0], value[1]); + GregorianCalendar timestamp = new GregorianCalendar( + year, + (value[2] & 0xff) - 1, + value[3] & 0xff, + value[4] & 0xff, + value[5] & 0xff, + value[6] & 0xff + ); + + if (honorDeviceTimeOffset) { + int offsetInHours = MiBandCoordinator.getDeviceTimeOffsetHours(); + if (offsetInHours != 0) { + timestamp.add(Calendar.HOUR_OF_DAY,-offsetInHours); + } + } + + return timestamp; + } + + return createCalendar(); + } + + public static int toUint16(byte... bytes) { + return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8); + } + + public static byte[] fromUint16(int value) { + return new byte[] { + (byte) (value & 0xff), + (byte) ((value >> 8) & 0xff), + }; + } + public static byte fromUint8(int value) { + return (byte) (value & 0xff); + } + + /** + * Creates a calendar object representing the current date and time. + */ + public static GregorianCalendar createCalendar() { + return new GregorianCalendar(); + } + + public static byte[] join(byte[] start, byte[] end) { + if (start == null || start.length == 0) { + return end; + } + if (end == null || end.length == 0) { + return start; + } + + byte[] result = new byte[start.length + end.length]; + System.arraycopy(start, 0, result, 0, start.length); + System.arraycopy(end, 0, result, start.length, end.length); + return result; + } + + public static byte[] calendarToLocalTimeBytes(GregorianCalendar now) { + byte[] result = new byte[2]; + result[0] = mapTimeZone(now.getTimeZone()); + result[1] = mapDstOffset(now); + return result; + } + + /** + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.time_zone.xml + * @param timeZone + * @return sint8 value from -48..+56 + */ + public static byte mapTimeZone(TimeZone timeZone) { + int utcOffsetInHours = (timeZone.getRawOffset() / (1000 * 60 * 60)); + return (byte) (utcOffsetInHours * 4); + } + + /** + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.dst_offset.xml + * @param now + * @return the DST offset for the given time; 0 if none; 255 if unknown + */ + public static byte mapDstOffset(Calendar now) { + TimeZone timeZone = now.getTimeZone(); + int dstSavings = timeZone.getDSTSavings(); + if (dstSavings == 0) { + return 0; + } + if (timeZone.inDaylightTime(now.getTime())) { + int dstInMinutes = dstSavings / (1000 * 60); + switch (dstInMinutes) { + case 30: + return 2; + case 60: + return 4; + case 120: + return 8; + } + return fromUint8(255); // unknown + } + return 0; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java new file mode 100644 index 00000000..c48a362a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java @@ -0,0 +1,203 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle; + +import android.util.SparseArray; + +import java.util.HashMap; + +public class BleNamesResolver { + private static HashMap mServices = new HashMap(); + private static HashMap mCharacteristics = new HashMap(); + private static SparseArray mValueFormats = new SparseArray(); + private static SparseArray mAppearance = new SparseArray(); + private static SparseArray mHeartRateSensorLocation = new SparseArray(); + + static public String resolveServiceName(final String uuid) + { + String result = mServices.get(uuid); + if(result == null) result = "Unknown Service"; + return result; + } + + static public String resolveValueTypeDescription(final int format) + { + Integer tmp = Integer.valueOf(format); + return mValueFormats.get(tmp, "Unknown Format"); + } + + static public String resolveCharacteristicName(final String uuid) + { + String result = mCharacteristics.get(uuid); + if(result == null) result = "Unknown Characteristic"; + return result; + } + + static public String resolveUuid(final String uuid) { + String result = mServices.get(uuid); + if(result != null) return "Service: " + result; + + result = mCharacteristics.get(uuid); + if(result != null) return "Characteristic: " + result; + + result = "Unknown UUID"; + return result; + } + + static public String resolveAppearance(int key) { + Integer tmp = Integer.valueOf(key); + return mAppearance.get(tmp, "Unknown Appearance"); + } + + static public String resolveHeartRateSensorLocation(int key) { + Integer tmp = Integer.valueOf(key); + return mHeartRateSensorLocation.get(tmp, "Other"); + } + + static public boolean isService(final String uuid) { + return mServices.containsKey(uuid); + } + + static public boolean isCharacteristic(final String uuid) { + return mCharacteristics.containsKey(uuid); + } + + static { + mServices.put("00001811-0000-1000-8000-00805f9b34fb", "Alert Notification Service"); + mServices.put("0000180f-0000-1000-8000-00805f9b34fb", "Battery Service"); + mServices.put("00001810-0000-1000-8000-00805f9b34fb", "Blood Pressure"); + mServices.put("00001805-0000-1000-8000-00805f9b34fb", "Current Time Service"); + mServices.put("00001818-0000-1000-8000-00805f9b34fb", "Cycling Power"); + mServices.put("00001816-0000-1000-8000-00805f9b34fb", "Cycling Speed and Cadence"); + mServices.put("0000180a-0000-1000-8000-00805f9b34fb", "Device Information"); + mServices.put("00001800-0000-1000-8000-00805f9b34fb", "Generic Access"); + mServices.put("00001801-0000-1000-8000-00805f9b34fb", "Generic Attribute"); + mServices.put("00001808-0000-1000-8000-00805f9b34fb", "Glucose"); + mServices.put("00001809-0000-1000-8000-00805f9b34fb", "Health Thermometer"); + mServices.put("0000180d-0000-1000-8000-00805f9b34fb", "Heart Rate"); + mServices.put("00001812-0000-1000-8000-00805f9b34fb", "Human Interface Device"); + mServices.put("00001802-0000-1000-8000-00805f9b34fb", "Immediate Alert"); + mServices.put("00001803-0000-1000-8000-00805f9b34fb", "Link Loss"); + mServices.put("00001819-0000-1000-8000-00805f9b34fb", "Location and Navigation"); + mServices.put("00001807-0000-1000-8000-00805f9b34fb", "Next DST Change Service"); + mServices.put("0000180e-0000-1000-8000-00805f9b34fb", "Phone Alert Status Service"); + mServices.put("00001806-0000-1000-8000-00805f9b34fb", "Reference Time Update Service"); + mServices.put("00001814-0000-1000-8000-00805f9b34fb", "Running Speed and Cadence"); + mServices.put("00001813-0000-1000-8000-00805f9b34fb", "Scan Parameters"); + mServices.put("00001804-0000-1000-8000-00805f9b34fb", "Tx Power"); + mServices.put("0000fee0-0000-3512-2118-0009af100700", "(Propr: Xiaomi MiLi Service)"); + mServices.put("00001530-0000-3512-2118-0009af100700", "(Propr: Xiaomi Weight Service)"); + + + mCharacteristics.put("00002a43-0000-1000-8000-00805f9b34fb", "Alert AlertCategory ID"); + mCharacteristics.put("00002a42-0000-1000-8000-00805f9b34fb", "Alert AlertCategory ID Bit Mask"); + mCharacteristics.put("00002a06-0000-1000-8000-00805f9b34fb", "Alert Level"); + mCharacteristics.put("00002a44-0000-1000-8000-00805f9b34fb", "Alert Notification Control Point"); + mCharacteristics.put("00002a3f-0000-1000-8000-00805f9b34fb", "Alert Status"); + mCharacteristics.put("00002a01-0000-1000-8000-00805f9b34fb", "Appearance"); + mCharacteristics.put("00002a19-0000-1000-8000-00805f9b34fb", "Battery Level"); + mCharacteristics.put("00002a49-0000-1000-8000-00805f9b34fb", "Blood Pressure Feature"); + mCharacteristics.put("00002a35-0000-1000-8000-00805f9b34fb", "Blood Pressure Measurement"); + mCharacteristics.put("00002a38-0000-1000-8000-00805f9b34fb", "Body Sensor Location"); + mCharacteristics.put("00002a22-0000-1000-8000-00805f9b34fb", "Boot Keyboard Input Report"); + mCharacteristics.put("00002a32-0000-1000-8000-00805f9b34fb", "Boot Keyboard Output Report"); + mCharacteristics.put("00002a33-0000-1000-8000-00805f9b34fb", "Boot Mouse Input Report"); + mCharacteristics.put("00002a5c-0000-1000-8000-00805f9b34fb", "CSC Feature"); + mCharacteristics.put("00002a5b-0000-1000-8000-00805f9b34fb", "CSC Measurement"); + mCharacteristics.put("00002a2b-0000-1000-8000-00805f9b34fb", "Current Time"); + mCharacteristics.put("00002a66-0000-1000-8000-00805f9b34fb", "Cycling Power Control Point"); + mCharacteristics.put("00002a65-0000-1000-8000-00805f9b34fb", "Cycling Power Feature"); + mCharacteristics.put("00002a63-0000-1000-8000-00805f9b34fb", "Cycling Power Measurement"); + mCharacteristics.put("00002a64-0000-1000-8000-00805f9b34fb", "Cycling Power Vector"); + mCharacteristics.put("00002a08-0000-1000-8000-00805f9b34fb", "Date Time"); + mCharacteristics.put("00002a0a-0000-1000-8000-00805f9b34fb", "Day Date Time"); + mCharacteristics.put("00002a09-0000-1000-8000-00805f9b34fb", "Day of Week"); + mCharacteristics.put("00002a00-0000-1000-8000-00805f9b34fb", "Device Name"); + mCharacteristics.put("00002a0d-0000-1000-8000-00805f9b34fb", "DST Offset"); + mCharacteristics.put("00002a0c-0000-1000-8000-00805f9b34fb", "Exact Time 256"); + mCharacteristics.put("00002a26-0000-1000-8000-00805f9b34fb", "Firmware Revision String"); + mCharacteristics.put("00002a51-0000-1000-8000-00805f9b34fb", "Glucose Feature"); + mCharacteristics.put("00002a18-0000-1000-8000-00805f9b34fb", "Glucose Measurement"); + mCharacteristics.put("00002a34-0000-1000-8000-00805f9b34fb", "Glucose Measurement Context"); + mCharacteristics.put("00002a27-0000-1000-8000-00805f9b34fb", "Hardware Revision String"); + mCharacteristics.put("00002a39-0000-1000-8000-00805f9b34fb", "Heart Rate Control Point"); + mCharacteristics.put("00002a37-0000-1000-8000-00805f9b34fb", "Heart Rate Measurement"); + mCharacteristics.put("00002a4c-0000-1000-8000-00805f9b34fb", "HID Control Point"); + mCharacteristics.put("00002a4a-0000-1000-8000-00805f9b34fb", "HID Information"); + mCharacteristics.put("00002a2a-0000-1000-8000-00805f9b34fb", "IEEE 11073-20601 Regulatory Certification Data List"); + mCharacteristics.put("00002a36-0000-1000-8000-00805f9b34fb", "Intermediate Cuff Pressure"); + mCharacteristics.put("00002a1e-0000-1000-8000-00805f9b34fb", "Intermediate Temperature"); + mCharacteristics.put("00002a6b-0000-1000-8000-00805f9b34fb", "LN Control Point"); + mCharacteristics.put("00002a6a-0000-1000-8000-00805f9b34fb", "LN Feature"); + mCharacteristics.put("00002a0f-0000-1000-8000-00805f9b34fb", "Local Time Information"); + mCharacteristics.put("00002a67-0000-1000-8000-00805f9b34fb", "Location and Speed"); + mCharacteristics.put("00002a29-0000-1000-8000-00805f9b34fb", "Manufacturer Name String"); + mCharacteristics.put("00002a21-0000-1000-8000-00805f9b34fb", "Measurement Interval"); + mCharacteristics.put("00002a24-0000-1000-8000-00805f9b34fb", "Model Number String"); + mCharacteristics.put("00002a68-0000-1000-8000-00805f9b34fb", "Navigation"); + mCharacteristics.put("00002a46-0000-1000-8000-00805f9b34fb", "New Alert"); + mCharacteristics.put("00002a04-0000-1000-8000-00805f9b34fb", "Peripheral Preferred Connection Parameters"); + mCharacteristics.put("00002a02-0000-1000-8000-00805f9b34fb", "Peripheral Privacy Flag"); + mCharacteristics.put("00002a50-0000-1000-8000-00805f9b34fb", "PnP ID"); + mCharacteristics.put("00002a69-0000-1000-8000-00805f9b34fb", "Position Quality"); + mCharacteristics.put("00002a4e-0000-1000-8000-00805f9b34fb", "Protocol Mode"); + mCharacteristics.put("00002a03-0000-1000-8000-00805f9b34fb", "Reconnection Address"); + mCharacteristics.put("00002a52-0000-1000-8000-00805f9b34fb", "Record Access Control Point"); + mCharacteristics.put("00002a14-0000-1000-8000-00805f9b34fb", "Reference Time Information"); + mCharacteristics.put("00002a4d-0000-1000-8000-00805f9b34fb", "Report"); + mCharacteristics.put("00002a4b-0000-1000-8000-00805f9b34fb", "Report Map"); + mCharacteristics.put("00002a40-0000-1000-8000-00805f9b34fb", "Ringer Control Point"); + mCharacteristics.put("00002a41-0000-1000-8000-00805f9b34fb", "Ringer Setting"); + mCharacteristics.put("00002a54-0000-1000-8000-00805f9b34fb", "RSC Feature"); + mCharacteristics.put("00002a53-0000-1000-8000-00805f9b34fb", "RSC Measurement"); + mCharacteristics.put("00002a55-0000-1000-8000-00805f9b34fb", "SC Control Point"); + mCharacteristics.put("00002a4f-0000-1000-8000-00805f9b34fb", "Scan Interval Window"); + mCharacteristics.put("00002a31-0000-1000-8000-00805f9b34fb", "Scan Refresh"); + mCharacteristics.put("00002a5d-0000-1000-8000-00805f9b34fb", "Sensor Location"); + mCharacteristics.put("00002a25-0000-1000-8000-00805f9b34fb", "Serial Number String"); + mCharacteristics.put("00002a05-0000-1000-8000-00805f9b34fb", "Service Changed"); + mCharacteristics.put("00002a28-0000-1000-8000-00805f9b34fb", "Software Revision String"); + mCharacteristics.put("00002a47-0000-1000-8000-00805f9b34fb", "Supported New Alert AlertCategory"); + mCharacteristics.put("00002a48-0000-1000-8000-00805f9b34fb", "Supported Unread Alert AlertCategory"); + mCharacteristics.put("00002a23-0000-1000-8000-00805f9b34fb", "System ID"); + mCharacteristics.put("00002a1c-0000-1000-8000-00805f9b34fb", "Temperature Measurement"); + mCharacteristics.put("00002a1d-0000-1000-8000-00805f9b34fb", "Temperature Type"); + mCharacteristics.put("00002a12-0000-1000-8000-00805f9b34fb", "Time Accuracy"); + mCharacteristics.put("00002a13-0000-1000-8000-00805f9b34fb", "Time Source"); + mCharacteristics.put("00002a16-0000-1000-8000-00805f9b34fb", "Time Update Control Point"); + mCharacteristics.put("00002a17-0000-1000-8000-00805f9b34fb", "Time Update State"); + mCharacteristics.put("00002a11-0000-1000-8000-00805f9b34fb", "Time with DST"); + mCharacteristics.put("00002a0e-0000-1000-8000-00805f9b34fb", "Time Zone"); + mCharacteristics.put("00002a07-0000-1000-8000-00805f9b34fb", "Tx Power Level"); + mCharacteristics.put("00002a45-0000-1000-8000-00805f9b34fb", "Unread Alert Status"); + + mValueFormats.put(Integer.valueOf(52), "32bit float"); + mValueFormats.put(Integer.valueOf(50), "16bit float"); + mValueFormats.put(Integer.valueOf(34), "16bit signed int"); + mValueFormats.put(Integer.valueOf(36), "32bit signed int"); + mValueFormats.put(Integer.valueOf(33), "8bit signed int"); + mValueFormats.put(Integer.valueOf(18), "16bit unsigned int"); + mValueFormats.put(Integer.valueOf(20), "32bit unsigned int"); + mValueFormats.put(Integer.valueOf(17), "8bit unsigned int"); + + // lets add also couple appearance string description + // https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml + mAppearance.put(Integer.valueOf(833), "Heart Rate Sensor: Belt"); + mAppearance.put(Integer.valueOf(832), "Generic Heart Rate Sensor"); + mAppearance.put(Integer.valueOf(0), "Unknown"); + mAppearance.put(Integer.valueOf(64), "Generic Phone"); + mAppearance.put(Integer.valueOf(1157), "Cycling: Speed and Cadence Sensor"); + mAppearance.put(Integer.valueOf(1152), "General Cycling"); + mAppearance.put(Integer.valueOf(1153), "Cycling Computer"); + mAppearance.put(Integer.valueOf(1154), "Cycling: Speed Sensor"); + mAppearance.put(Integer.valueOf(1155), "Cycling: Cadence Sensor"); + mAppearance.put(Integer.valueOf(1156), "Cycling: Speed and Cadence Sensor"); + mAppearance.put(Integer.valueOf(1157), "Cycling: Power Sensor"); + + mHeartRateSensorLocation.put(Integer.valueOf(0), "Other"); + mHeartRateSensorLocation.put(Integer.valueOf(1), "Chest"); + mHeartRateSensorLocation.put(Integer.valueOf(2), "Wrist"); + mHeartRateSensorLocation.put(Integer.valueOf(3), "Finger"); + mHeartRateSensorLocation.put(Integer.valueOf(4), "Hand"); + mHeartRateSensorLocation.put(Integer.valueOf(5), "Ear Lobe"); + mHeartRateSensorLocation.put(Integer.valueOf(6), "Foot"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEAction.java index faf35aa6..db2587c6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEAction.java @@ -25,7 +25,7 @@ public abstract class BtLEAction { } /** - * Returns true if this actions expects an (async) result which must + * Returns true if this action expects an (async) result which must * be waited for, before continuing with other actions. *

* This is needed because the current Bluedroid stack can only deal diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java index fc7358a8..f81285d9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java @@ -14,6 +14,7 @@ import android.support.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.BlockingQueue; @@ -47,7 +48,7 @@ public final class BtLEQueue { private final InternalGattCallback internalGattCallback; private boolean mAutoReconnect; - private Thread dispatchThread = new Thread("GadgetBridge GATT Dispatcher") { + private Thread dispatchThread = new Thread("Gadgetbridge GATT Dispatcher") { @Override public void run() { @@ -147,7 +148,7 @@ public final class BtLEQueue { } synchronized (mGattMonitor) { if (mBluetoothGatt != null) { - // Tribal knowledge says you're better off not reusing existing BlueoothGatt connections, + // Tribal knowledge says you're better off not reusing existing BluetoothGatt connections, // so create a new one. LOG.info("connect() requested -- disconnecting previous connection: " + mGbDevice.getName()); disconnect(); @@ -198,6 +199,7 @@ public final class BtLEQueue { if (mWaitForActionResultLatch != null) { mWaitForActionResultLatch.countDown(); } + boolean wasInitialized = mGbDevice.isInitialized(); setDeviceConnectionState(State.NOT_CONNECTED); // either we've been disconnected because the device is out of range @@ -207,7 +209,7 @@ public final class BtLEQueue { // reconnecting automatically, so we try to fix this by re-creating mBluetoothGatt. // Not sure if this actually works without re-initializing the device... if (status != 0) { - if (!maybeReconnect()) { + if (!wasInitialized || !maybeReconnect()) { disconnect(); // ensure that we start over cleanly next time } } @@ -257,6 +259,23 @@ public final class BtLEQueue { } } + /** + * Adds a transaction to the beginning of the queue. + * Note that actions of the *currently executing* transaction + * will still be executed before the given transaction. + * + * @param transaction + */ + public void insert(Transaction transaction) { + LOG.debug("about to insert: " + transaction); + if (!transaction.isEmpty()) { + List tail = new ArrayList<>(mTransactions.size() + 2); + mTransactions.drainTo(tail); + mTransactions.add(transaction); + mTransactions.addAll(tail); + } + } + public void clear() { mTransactions.clear(); } @@ -329,8 +348,14 @@ public final class BtLEQueue { LOG.info("Connected to GATT server."); setDeviceConnectionState(State.CONNECTED); // Attempts to discover services after successful connection. - LOG.info("Attempting to start service discovery:" + - gatt.discoverServices()); + List cachedServices = gatt.getServices(); + if (cachedServices != null && cachedServices.size() > 0) { + LOG.info("Using cached services, skipping discovery"); + onServicesDiscovered(gatt, BluetoothGatt.GATT_SUCCESS); + } else { + LOG.info("Attempting to start service discovery:" + + gatt.discoverServices()); + } break; case BluetoothProfile.STATE_DISCONNECTED: LOG.info("Disconnected from GATT server."); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java index 3089856b..107af38d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCallback.java @@ -25,6 +25,10 @@ import android.bluetooth.BluetoothGattDescriptor; * Callback interface handling gatt events. * Pretty much the same as {@link BluetoothGattCallback}, except it's an interface * instead of an abstract class. Some handlers commented out, because not used (yet). + * + * Note: the boolean return values indicate whether this callback "consumed" this event + * or not. True means, the event was consumed by this instance and no further instances + * shall be notified. Fallse means, this instance could not handle the event. */ public interface GattCallback { @@ -48,7 +52,7 @@ public interface GattCallback { * @param status * @see BluetoothGattCallback#onCharacteristicRead(BluetoothGatt, BluetoothGattCharacteristic, int) */ - void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status); + boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status); /** * @param gatt @@ -56,7 +60,7 @@ public interface GattCallback { * @param status * @see BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int) */ - void onCharacteristicWrite(BluetoothGatt gatt, + boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status); /** @@ -64,7 +68,7 @@ public interface GattCallback { * @param characteristic * @see BluetoothGattCallback#onCharacteristicChanged(BluetoothGatt, BluetoothGattCharacteristic) */ - void onCharacteristicChanged(BluetoothGatt gatt, + boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic); /** @@ -73,7 +77,7 @@ public interface GattCallback { * @param status * @see BluetoothGattCallback#onDescriptorRead(BluetoothGatt, BluetoothGattDescriptor, int) */ - void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, + boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status); /** @@ -82,7 +86,7 @@ public interface GattCallback { * @param status * @see BluetoothGattCallback#onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int) */ - void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, + boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status); // // /** diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java index afbd9ee6..812454a4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java @@ -1,5 +1,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle; +import android.bluetooth.BluetoothGattCharacteristic; + import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -182,8 +184,8 @@ public class GattCharacteristic { static { GATTCHARACTERISTIC_DEBUG = new HashMap<>(); - GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_CATEGORY_ID, "Alert Category ID"); - GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_CATEGORY_ID_BIT_MASK, "Alert Category ID Bit Mask"); + GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_CATEGORY_ID, "Alert AlertCategory ID"); + GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_CATEGORY_ID_BIT_MASK, "Alert AlertCategory ID Bit Mask"); GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_LEVEL, "Alert Level"); GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_NOTIFICATION_CONTROL_POINT, "Alert Notification Control Point"); GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_ALERT_STATUS, "Alert Status"); @@ -219,8 +221,8 @@ public class GattCharacteristic { GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING, "Serial Number String"); GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_GATT_SERVICE_CHANGED, "Service Changed"); GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING, "Software Revision String"); - GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SUPPORTED_NEW_ALERT_CATEGORY, "Supported New Alert Category"); - GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SUPPORTED_UNREAD_ALERT_CATEGORY, "Supported Unread Alert Category"); + GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SUPPORTED_NEW_ALERT_CATEGORY, "Supported New Alert AlertCategory"); + GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SUPPORTED_UNREAD_ALERT_CATEGORY, "Supported Unread Alert AlertCategory"); GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_SYSTEM_ID, "System ID"); GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TEMPERATURE_MEASUREMENT, "Temperature Measurement"); GATTCHARACTERISTIC_DEBUG.put(UUID_CHARACTERISTIC_TEMPERATURE_TYPE, "Temperature DeviceType"); @@ -241,4 +243,7 @@ public class GattCharacteristic { return name; } + public static String toString(BluetoothGattCharacteristic characteristic) { + return characteristic.getUuid() + " (" + lookup(characteristic.getUuid(), "unknown") + ")"; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java index 79773031..4e5b4eb9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/NotifyAction.java @@ -9,7 +9,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; -import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.UUID_DESCRIPTOR_GATT_CLIENT_CHARACTERISTIC_CONFIGURATION; @@ -20,7 +19,7 @@ import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.U */ public class NotifyAction extends BtLEAction { - private static final Logger LOG = LoggerFactory.getLogger(TransactionBuilder.class); + private static final Logger LOG = LoggerFactory.getLogger(NotifyAction.class); protected final boolean enableFlag; private boolean hasWrittenDescriptor = true; @@ -49,7 +48,7 @@ public class NotifyAction extends BtLEAction { hasWrittenDescriptor = false; } } else { - LOG.warn("sleep descriptor null"); + LOG.warn("Descriptor CLIENT_CHARACTERISTIC_CONFIGURATION for characteristic " + getCharacteristic().getUuid() + " is null"); hasWrittenDescriptor = false; } } else { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java index 5cecee06..95e765f9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java @@ -4,6 +4,10 @@ import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; /** @@ -12,6 +16,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; * {@link BluetoothGattCallback} */ public class WriteAction extends BtLEAction { + private static final Logger LOG = LoggerFactory.getLogger(WriteAction.class); private final byte[] value; @@ -24,7 +29,7 @@ public class WriteAction extends BtLEAction { public boolean run(BluetoothGatt gatt) { BluetoothGattCharacteristic characteristic = getCharacteristic(); int properties = characteristic.getProperties(); - //TODO: expectsResult should return false if PROPERTY_WRITE_NO_RESPONSE is true, but this yelds to timing issues + //TODO: expectsResult should return false if PROPERTY_WRITE_NO_RESPONSE is true, but this leads to timing issues if ((properties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0 || ((properties & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0)) { return writeValue(gatt, characteristic, value); } @@ -32,6 +37,9 @@ public class WriteAction extends BtLEAction { } protected boolean writeValue(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) { + if (LOG.isDebugEnabled()) { + LOG.debug("writing to characteristic: " + characteristic.getUuid() + ": " + Logging.formatBytes(value)); + } if (characteristic.setValue(value)) { return gatt.writeCharacteristic(characteristic); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java new file mode 100644 index 00000000..50122b41 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java @@ -0,0 +1,73 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; + +import java.io.IOException; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractGattCallback; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; + +/** + * Base class for all BLE profiles, with things that all impplementations are + * expected to use. + * + * Instances are used in the context of a concrete AbstractBTLEDeviceSupport instance, + * i.e. a concrete device. + * + * @see nodomain.freeyourgadget.gadgetbridge.service.btle.GattService + * @see nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic + * @see https://www.bluetooth.com/specifications/assigned-numbers + */ +public abstract class AbstractBleProfile extends AbstractGattCallback { + private final T mSupport; + + public AbstractBleProfile(T support) { + this.mSupport = support; + } + + /** + * All notifications should be sent through this methods to make them testable. + * @param intent the intent to broadcast + */ + protected void notify(Intent intent) { + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + + /** + * Delegates to the DeviceSupport instance and additionally sets this instance as the Gatt + * callback for the transaction. + * + * @param taskName + * @return + * @throws IOException + */ + public TransactionBuilder performInitialized(String taskName) throws IOException { + TransactionBuilder builder = mSupport.performInitialized(taskName); + builder.setGattCallback(this); + return builder; + } + + public Context getContext() { + return mSupport.getContext(); + } + + protected GBDevice getDevice() { + return mSupport.getDevice(); + } + + protected BluetoothGattCharacteristic getCharacteristic(UUID uuid) { + return mSupport.getCharacteristic(uuid); + } + + protected BtLEQueue getQueue() { + return mSupport.getQueue(); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java new file mode 100644 index 00000000..1e7d2bca --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java @@ -0,0 +1,21 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles; + +import android.bluetooth.BluetoothGattCharacteristic; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; + +public class ValueDecoder { + private static final Logger LOG = LoggerFactory.getLogger(ValueDecoder.class); + + public static int decodePercent(BluetoothGattCharacteristic characteristic) { + int percent = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); + if (percent > 100 || percent < 0) { + LOG.warn("Unexpected percent value: " + percent + ": " + GattCharacteristic.toString(characteristic)); + percent = Math.max(100, Math.min(0, percent)); + } + return percent; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertCategory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertCategory.java new file mode 100644 index 00000000..393a34db --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertCategory.java @@ -0,0 +1,79 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification; + +import java.util.ArrayList; +import java.util.List; + +/** + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_category_id.xml + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_category_id_bit_mask.xml + */ +public enum AlertCategory { + Simple(0), + Email(1), + News(2), + IncomingCall(3), + MissedCall(4), + SMS(5), + VoiceMail(6), + Schedule(7), + HighPriorityAlert(8), + InstantMessage(9); + // 10-250 reserved for future use + // 251-255 defined by service specification + + private final int id; + + AlertCategory(int id) { + this.id = id; + } + + /** + * Returns the numerical ID value of this category + * To be used as uint8 value + * @return the uint8 value for this category + */ + public int getId() { + return id; + } + + private int realBitNumber() { + // the ID corresponds to the bit for the bitset + return id; + } + + private int bitNumberPerByte() { + // the ID corresponds to the bit for the bitset (per byte) + return realBitNumber() % 8; + } + + private int asBit() { + return 1 << bitNumberPerByte(); + } + + private int byteNumber() { + return id <= 7 ? 0 : 1; + } + + /** + * Converts the given categories to an array of bytes. + * @param categories + * @return + */ + public static byte[] toBitmask(AlertCategory... categories) { + byte[] result = new byte[2]; + + for (AlertCategory category : categories) { + result[category.byteNumber()] |= category.asBit(); + } + return result; + } + +// SupportedNewAlertCategory + public static AlertCategory[] fromBitMask(byte[] bytes) { + List result = new ArrayList<>(); + byte b = bytes[0]; + + return null; + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertLevel.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertLevel.java new file mode 100644 index 00000000..dd6f22f1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertLevel.java @@ -0,0 +1,27 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification; + + +/** + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_level.xml + */ +public enum AlertLevel { + NoAlert(0), + MildAlert(1), + HighAlert(2); + // 3-255 reserved + + private final int id; + + AlertLevel(int id) { + this.id = id; + } + + /** + * The alert level ID + * To be used as uint8 value + * @return + */ + public int getId() { + return id; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationControl.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationControl.java new file mode 100644 index 00000000..48c88a9c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationControl.java @@ -0,0 +1,8 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification; + +/** + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_notification_control_point.xml + */ +public class AlertNotificationControl { + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationProfile.java new file mode 100644 index 00000000..1f37e42a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertNotificationProfile.java @@ -0,0 +1,12 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; + +public class AlertNotificationProfile extends AbstractBleProfile { + public AlertNotificationProfile(T support) { + super(support); + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertStatus.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertStatus.java new file mode 100644 index 00000000..38f2dd35 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/AlertStatus.java @@ -0,0 +1,21 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification; + +/** + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_status.xml + * uint8 value (bitmask) of the given values + */ +public class AlertStatus { + public static final int RINGER_ACTIVE_BIT = 1; + public static final int VIBRATE_ACTIVE = 1 << 1; + public static final int DISPLAY_ALERT_ACTIVE = 1 << 2; + + public static boolean isRingerActive(int status) { + return (status & RINGER_ACTIVE_BIT) == RINGER_ACTIVE_BIT; + } + public static boolean isVibrateActive(int status) { + return (status & VIBRATE_ACTIVE) == VIBRATE_ACTIVE; + } + public static boolean isDisplayAlertActive(int status) { + return (status & DISPLAY_ALERT_ACTIVE) == DISPLAY_ALERT_ACTIVE; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/SupportedNewAlertCategory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/SupportedNewAlertCategory.java new file mode 100644 index 00000000..325d6daf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/alertnotification/SupportedNewAlertCategory.java @@ -0,0 +1,58 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification; + +/** + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_category_id.xml + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_category_id_bit_mask.xml + */ +public class SupportedNewAlertCategory { + private final int id; +// +// public static Ca(byte[] categoryBytes) { +// +// } + + public SupportedNewAlertCategory(int id) { + this.id = id; + } + + /** + * Returns the numerical ID value of this category + * To be used as uint8 value + * @return the uint8 value for this category + */ + public int getId() { + return id; + } + + private int realBitNumber() { + // the ID corresponds to the bit for the bitset + return id; + } + + private int bitNumberPerByte() { + // the ID corresponds to the bit for the bitset (per byte) + return realBitNumber() % 8; + } + + private int asBit() { + return 1 << bitNumberPerByte(); + } + + private int byteNumber() { + return id <= 7 ? 0 : 1; + } + + /** + * Converts the given categories to an array of bytes. + * @param categories + * @return + */ + public static byte[] toBitmask(SupportedNewAlertCategory... categories) { + byte[] result = new byte[2]; + + for (SupportedNewAlertCategory category : categories) { + result[category.byteNumber()] |= category.asBit(); + } + return result; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfo.java new file mode 100644 index 00000000..f13c47bf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfo.java @@ -0,0 +1,46 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery; + +import android.os.Parcel; +import android.os.Parcelable; + +public class BatteryInfo implements Parcelable{ + + private int percentCharged; + + public BatteryInfo() { + } + + protected BatteryInfo(Parcel in) { + percentCharged = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(percentCharged); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public BatteryInfo createFromParcel(Parcel in) { + return new BatteryInfo(in); + } + + @Override + public BatteryInfo[] newArray(int size) { + return new BatteryInfo[size]; + } + }; + + public int getPercentCharged() { + return percentCharged; + } + + public void setPercentCharged(int percentCharged) { + this.percentCharged = percentCharged; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfoProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfoProfile.java new file mode 100644 index 00000000..160c83ef --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfoProfile.java @@ -0,0 +1,72 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.ValueDecoder; + +public class BatteryInfoProfile extends AbstractBleProfile { + private static final Logger LOG = LoggerFactory.getLogger(BatteryInfoProfile.class); + + private static final String ACTION_PREFIX = BatteryInfoProfile.class.getName() + "_"; + + public static final String ACTION_BATTERY_INFO = ACTION_PREFIX + "BATTERY_INFO"; + public static final String EXTRA_BATTERY_INFO = "BATTERY_INFO"; + + public static final UUID SERVICE_UUID = GattService.UUID_SERVICE_BATTERY_SERVICE; + + public static final UUID UUID_CHARACTERISTIC_BATTERY_LEVEL = GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL; + private final BatteryInfo batteryInfo = new BatteryInfo(); + + public BatteryInfoProfile(T support) { + super(support); + } + + public void requestBatteryInfo(TransactionBuilder builder) { + builder.read(getCharacteristic(UUID_CHARACTERISTIC_BATTERY_LEVEL)); + } + + public void enableNotifiy() { + // TODO: notification + } + + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + UUID charUuid = characteristic.getUuid(); + if (charUuid.equals(UUID_CHARACTERISTIC_BATTERY_LEVEL)) { + handleBatteryLevel(gatt, characteristic); + return true; + } else { + LOG.info("Unexpected onCharacteristicRead: " + GattCharacteristic.toString(characteristic)); + } + } else { + LOG.warn("error reading from characteristic:" + GattCharacteristic.toString(characteristic)); + } + return false; + } + + private void handleBatteryLevel(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + int percent = ValueDecoder.decodePercent(characteristic); + batteryInfo.setPercentCharged(percent); + + notify(createIntent(batteryInfo)); + } + + private Intent createIntent(BatteryInfo batteryInfo) { + Intent intent = new Intent(ACTION_BATTERY_INFO); + intent.putExtra(EXTRA_BATTERY_INFO, batteryInfo); + return intent; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfo.java new file mode 100644 index 00000000..de83b567 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfo.java @@ -0,0 +1,148 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo; + +import android.os.Parcel; +import android.os.Parcelable; + +public class DeviceInfo implements Parcelable{ + private String manufacturerName; + private String modelNumber; + private String serialNumber; + private String hardwareRevision; + private String firmwareRevision; + private String softwareRevision; + private String systemId; + private String regulatoryCertificationDataList; + private String pnpId; + + public DeviceInfo() { + } + + protected DeviceInfo(Parcel in) { + manufacturerName = in.readString(); + modelNumber = in.readString(); + serialNumber = in.readString(); + hardwareRevision = in.readString(); + firmwareRevision = in.readString(); + softwareRevision = in.readString(); + systemId = in.readString(); + regulatoryCertificationDataList = in.readString(); + pnpId = in.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(manufacturerName); + dest.writeString(modelNumber); + dest.writeString(serialNumber); + dest.writeString(hardwareRevision); + dest.writeString(firmwareRevision); + dest.writeString(softwareRevision); + dest.writeString(systemId); + dest.writeString(regulatoryCertificationDataList); + dest.writeString(pnpId); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public DeviceInfo createFromParcel(Parcel in) { + return new DeviceInfo(in); + } + + @Override + public DeviceInfo[] newArray(int size) { + return new DeviceInfo[size]; + } + }; + + public String getManufacturerName() { + return manufacturerName; + } + + public void setManufacturerName(String manufacturerName) { + this.manufacturerName = manufacturerName; + } + + public String getModelNumber() { + return modelNumber; + } + + public void setModelNumber(String modelNumber) { + this.modelNumber = modelNumber; + } + + public String getSerialNumber() { + return serialNumber; + } + + public void setSerialNumber(String serialNumber) { + this.serialNumber = serialNumber; + } + + public String getHardwareRevision() { + return hardwareRevision; + } + + public void setHardwareRevision(String hardwareRevision) { + this.hardwareRevision = hardwareRevision; + } + + public String getFirmwareRevision() { + return firmwareRevision; + } + + public void setFirmwareRevision(String firmwareRevision) { + this.firmwareRevision = firmwareRevision; + } + + public String getSoftwareRevision() { + return softwareRevision; + } + + public void setSoftwareRevision(String softwareRevision) { + this.softwareRevision = softwareRevision; + } + + public String getSystemId() { + return systemId; + } + + public void setSystemId(String systemId) { + this.systemId = systemId; + } + + public String getRegulatoryCertificationDataList() { + return regulatoryCertificationDataList; + } + + public void setRegulatoryCertificationDataList(String regulatoryCertificationDataList) { + this.regulatoryCertificationDataList = regulatoryCertificationDataList; + } + + public String getPnpId() { + return pnpId; + } + + public void setPnpId(String pnpId) { + this.pnpId = pnpId; + } + + @Override + public String toString() { + return "DeviceInfo{" + + "manufacturerName='" + manufacturerName + '\'' + + ", modelNumber='" + modelNumber + '\'' + + ", serialNumber='" + serialNumber + '\'' + + ", hardwareRevision='" + hardwareRevision + '\'' + + ", firmwareRevision='" + firmwareRevision + '\'' + + ", softwareRevision='" + softwareRevision + '\'' + + ", systemId='" + systemId + '\'' + + ", regulatoryCertificationDataList='" + regulatoryCertificationDataList + '\'' + + ", pnpId='" + pnpId + '\'' + + '}'; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfoProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfoProfile.java new file mode 100644 index 00000000..b312249f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfoProfile.java @@ -0,0 +1,162 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; + +public class DeviceInfoProfile extends AbstractBleProfile { + private static final Logger LOG = LoggerFactory.getLogger(DeviceInfoProfile.class); + + private static final String ACTION_PREFIX = DeviceInfoProfile.class.getName() + "_"; + + public static final String ACTION_DEVICE_INFO = ACTION_PREFIX + "DEVICE_INFO"; + public static final String EXTRA_DEVICE_INFO = "DEVICE_INFO"; + + public static final UUID SERVICE_UUID = GattService.UUID_SERVICE_DEVICE_INFORMATION; + + public static final UUID UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING = GattCharacteristic.UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING; + public static final UUID UUID_CHARACTERISTIC_MODEL_NUMBER_STRING = GattCharacteristic.UUID_CHARACTERISTIC_MODEL_NUMBER_STRING; + public static final UUID UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING = GattCharacteristic.UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING; + public static final UUID UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING = GattCharacteristic.UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING; + public static final UUID UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING = GattCharacteristic.UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING; + public static final UUID UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING = GattCharacteristic.UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING; + public static final UUID UUID_CHARACTERISTIC_SYSTEM_ID = GattCharacteristic.UUID_CHARACTERISTIC_SYSTEM_ID; + public static final UUID UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST = GattCharacteristic.UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST; + public static final UUID UUID_CHARACTERISTIC_PNP_ID = GattCharacteristic.UUID_CHARACTERISTIC_PNP_ID; + private final DeviceInfo deviceInfo = new DeviceInfo(); + + public DeviceInfoProfile(T support) { + super(support); + } + + public void requestDeviceInfo(TransactionBuilder builder) { + builder.read(getCharacteristic(UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_MODEL_NUMBER_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_SYSTEM_ID)) + .read(getCharacteristic(UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST)) + .read(getCharacteristic(UUID_CHARACTERISTIC_PNP_ID)); + } + + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + UUID charUuid = characteristic.getUuid(); + if (charUuid.equals(UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING)) { + handleManufacturerName(gatt, characteristic); + return true; + } else if (charUuid.equals(UUID_CHARACTERISTIC_MODEL_NUMBER_STRING)) { + handleModelNumber(gatt, characteristic); + return true; + } else if (charUuid.equals(UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING)) { + handleSerialNumber(gatt, characteristic); + return true; + } else if (charUuid.equals(UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING)) { + handleHardwareRevision(gatt, characteristic); + return true; + } else if (charUuid.equals(UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING)) { + handleFirmwareRevision(gatt, characteristic); + return true; + } else if (charUuid.equals(UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING)) { + handleSoftwareRevision(gatt, characteristic); + return true; + } else if (charUuid.equals(UUID_CHARACTERISTIC_SYSTEM_ID)) { + handleSystemId(gatt, characteristic); + return true; + } else if (charUuid.equals(UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST)) { + handleRegulatoryCertificationData(gatt, characteristic); + return true; + } else if (charUuid.equals(UUID_CHARACTERISTIC_PNP_ID)) { + handlePnpId(gatt, characteristic); + return true; + } else { + LOG.info("Unexpected onCharacteristicRead: " + GattCharacteristic.toString(characteristic)); + } + } else { + LOG.warn("error reading from characteristic:" + GattCharacteristic.toString(characteristic)); + } + return false; + } + + + private void handleManufacturerName(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String name = characteristic.getStringValue(0); + deviceInfo.setManufacturerName(name); + notify(createIntent(deviceInfo)); + } + + private void handleModelNumber(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String modelNumber = characteristic.getStringValue(0); + deviceInfo.setModelNumber(modelNumber); + notify(createIntent(deviceInfo)); + } + private void handleSerialNumber(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String serialNumber = characteristic.getStringValue(0); + deviceInfo.setSerialNumber(serialNumber); + notify(createIntent(deviceInfo)); + } + + private void handleHardwareRevision(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String hardwareRevision = characteristic.getStringValue(0); + deviceInfo.setHardwareRevision(hardwareRevision); + notify(createIntent(deviceInfo)); + } + + private void handleFirmwareRevision(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String firmwareRevision = characteristic.getStringValue(0); + deviceInfo.setFirmwareRevision(firmwareRevision); + notify(createIntent(deviceInfo)); + } + + private void handleSoftwareRevision(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String softwareRevision = characteristic.getStringValue(0); + deviceInfo.setSoftwareRevision(softwareRevision); + notify(createIntent(deviceInfo)); + } + + private void handleSystemId(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String systemId = characteristic.getStringValue(0); + deviceInfo.setSystemId(systemId); + notify(createIntent(deviceInfo)); + } + + private void handleRegulatoryCertificationData(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + // TODO: regulatory certification data list not supported yet +// String regulatoryCertificationData = characteristic.getStringValue(0); +// deviceInfo.setRegulatoryCertificationDataList(regulatoryCertificationData); +// notify(createIntent(deviceInfo)); + } + + private void handlePnpId(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + byte[] value = characteristic.getValue(); + if (value.length == 7) { +// int vendorSource +// +// deviceInfo.setPnpId(pnpId); + notify(createIntent(deviceInfo)); + } else { + // TODO: LOG warning + } + } + + private Intent createIntent(DeviceInfo deviceInfo) { + Intent intent = new Intent(ACTION_DEVICE_INFO); + intent.putExtra(EXTRA_DEVICE_INFO, deviceInfo); // TODO: broadcast a clone of the info + return intent; + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/BodySensorLocation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/BodySensorLocation.java new file mode 100644 index 00000000..4b71c507 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/BodySensorLocation.java @@ -0,0 +1,23 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate; + +/** + * The Body Sensor Location characteristic of the device is used to describe the intended location of the heart rate measurement for the device. + * + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.body_sensor_location.xml + */ +public enum BodySensorLocation { + Other(0), + Chest(1), + Wrist(2), + Finger(3), + Hand(4), + EarLobe(5), + Foot(6); + // others are reserved + + private final int val; + + BodySensorLocation(int val) { + this.val = val; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/HeartRateProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/HeartRateProfile.java new file mode 100644 index 00000000..ec195fce --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/HeartRateProfile.java @@ -0,0 +1,62 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml + */ +public class HeartRateProfile extends AbstractBleProfile { + private static final Logger LOG = LoggerFactory.getLogger(HeartRateProfile.class); + + /** + * Returned when a request to the heart rate control point is not supported by the device + */ + public static final int ERR_CONTROL_POINT_NOT_SUPPORTED = 0x80; + + public HeartRateProfile(T support) { + super(support); + } + + public void resetEnergyExpended(TransactionBuilder builder) { + writeToControlPoint((byte) 0x01, builder); + } + + protected void writeToControlPoint(byte value, TransactionBuilder builder) { + writeToControlPoint(new byte[] { value }, builder); + } + + protected void writeToControlPoint(byte[] value, TransactionBuilder builder) { + builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), value); + } + + public void requestBodySensorLocation(TransactionBuilder builder) { + + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { + int flag = characteristic.getProperties(); + int format = -1; + if ((flag & 0x01) != 0) { + format = BluetoothGattCharacteristic.FORMAT_UINT16; + } else { + format = BluetoothGattCharacteristic.FORMAT_UINT8; + } + final int heartRate = characteristic.getIntValue(format, 1); + LOG.info("Heart rate: " + heartRate, Toast.LENGTH_LONG, GB.INFO); + } + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewIoThread.java new file mode 100644 index 00000000..2034a07f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewIoThread.java @@ -0,0 +1,220 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.content.Context; +import android.os.ParcelUuid; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewConstants; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class LiveviewIoThread extends GBDeviceIoThread { + private static final Logger LOG = LoggerFactory.getLogger(LiveviewIoThread.class); + + private static final UUID SERIAL = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); + + private final LiveviewProtocol mLiveviewProtocol; + private final LiveviewSupport mLiveviewSupport; + + + private BluetoothAdapter mBtAdapter = null; + private BluetoothSocket mBtSocket = null; + private InputStream mInStream = null; + private OutputStream mOutStream = null; + private boolean mQuit = false; + + @Override + public void quit() { + mQuit = true; + if (mBtSocket != null) { + try { + mBtSocket.close(); + } catch (IOException e) { + LOG.error(e.getMessage()); + } + } + } + + private boolean mIsConnected = false; + + + public LiveviewIoThread(GBDevice gbDevice, Context context, GBDeviceProtocol lvProtocol, LiveviewSupport lvSupport, BluetoothAdapter lvBtAdapter) { + super(gbDevice, context); + mLiveviewProtocol = (LiveviewProtocol) lvProtocol; + mBtAdapter = lvBtAdapter; + mLiveviewSupport = lvSupport; + } + + @Override + public synchronized void write(byte[] bytes) { + if (null == bytes) + return; + LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length)); + try { + mOutStream.write(bytes); + mOutStream.flush(); + } catch (IOException e) { + LOG.error("Error writing.", e); + } + } + + @Override + public void run() { + mIsConnected = connect(); + if (!mIsConnected) { + setUpdateState(GBDevice.State.NOT_CONNECTED); + return; + } + mQuit = false; + + while (!mQuit) { + LOG.info("Ready for a new message exchange."); + + try { + GBDeviceEvent deviceEvents[] = mLiveviewProtocol.decodeResponse(parseIncoming()); + if (deviceEvents == null) { + LOG.info("unhandled message"); + } else { + for (GBDeviceEvent deviceEvent : deviceEvents) { + if (deviceEvent == null) { + continue; + } + mLiveviewSupport.evaluateGBDeviceEvent(deviceEvent); + } + } + } catch (SocketTimeoutException ignore) { + LOG.debug("socket timeout, we can't help but ignore this"); + } catch (IOException e) { + LOG.info(e.getMessage()); + mIsConnected = false; + mBtSocket = null; + mInStream = null; + mOutStream = null; + LOG.info("Bluetooth socket closed, will quit IO Thread"); + break; + } + } + + mIsConnected = false; + if (mBtSocket != null) { + try { + mBtSocket.close(); + } catch (IOException e) { + LOG.error(e.getMessage()); + } + mBtSocket = null; + } + setUpdateState(GBDevice.State.NOT_CONNECTED); + } + + @Override + protected boolean connect() { + GBDevice.State originalState = gbDevice.getState(); + setUpdateState(GBDevice.State.CONNECTING); + + try { + BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(gbDevice.getAddress()); + ParcelUuid uuids[] = btDevice.getUuids(); + if (uuids == null) { + return false; + } + for (ParcelUuid uuid : uuids) { + LOG.info("found service UUID " + uuid); + } + mBtSocket = btDevice.createRfcommSocketToServiceRecord(uuids[0].getUuid()); + mBtSocket.connect(); + mInStream = mBtSocket.getInputStream(); + mOutStream = mBtSocket.getOutputStream(); + setUpdateState(GBDevice.State.CONNECTED); + } catch (IOException e) { + LOG.error("Server socket cannot be started."); + //LOG.error(e.getMessage()); + setUpdateState(originalState); + mInStream = null; + mOutStream = null; + mBtSocket = null; + return false; + } + + write(mLiveviewProtocol.encodeSetTime()); + setUpdateState(GBDevice.State.INITIALIZED); + + return true; + } + + private void setUpdateState(GBDevice.State state) { + gbDevice.setState(state); + gbDevice.sendDeviceUpdateIntent(getContext()); + } + + private byte[] parseIncoming() throws IOException { + ByteArrayOutputStream msgStream = new ByteArrayOutputStream(); + + boolean finished = false; + ReaderState state = ReaderState.ID; + byte[] incoming = new byte[1]; + + while (!finished) { + mInStream.read(incoming); + msgStream.write(incoming); + + switch (state) { + case ID: + state = ReaderState.HEADER_LEN; + incoming = new byte[1]; + break; + case HEADER_LEN: + int headerSize = 0xff & incoming[0]; + if (headerSize < 0) + throw new IOException(); + state = ReaderState.HEADER; + incoming = new byte[headerSize]; + break; + case HEADER: + int payloadSize = getLastInt(msgStream); + if (payloadSize < 0 || payloadSize > 8000) //this will possibly be changed in the future + throw new IOException(); + state = ReaderState.PAYLOAD; + incoming = new byte[payloadSize]; + break; + case PAYLOAD: //read is blocking, if we are here we have all the data + finished = true; + break; + } + } + byte[] msgArray = msgStream.toByteArray(); + LOG.debug("received: " + GB.hexdump(msgArray, 0, msgArray.length)); + return msgArray; + } + + + /** + * Enumeration containing the possible internal status of the reader. + */ + private enum ReaderState { + ID, HEADER_LEN, HEADER, PAYLOAD; + } + + private int getLastInt(ByteArrayOutputStream stream) { + byte[] array = stream.toByteArray(); + ByteBuffer buffer = ByteBuffer.wrap(array, array.length - 4, 4); + buffer.order(LiveviewConstants.BYTE_ORDER); + return buffer.getInt(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewProtocol.java new file mode 100644 index 00000000..27d38d7c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewProtocol.java @@ -0,0 +1,132 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview; + +import java.nio.ByteBuffer; +import java.util.Calendar; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; +import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewConstants; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class LiveviewProtocol extends GBDeviceProtocol { + + @Override + public byte[] encodeFindDevice(boolean start) { + return encodeVibrateRequest((short) 100, (short) 200); + } + + protected LiveviewProtocol(GBDevice device) { + super(device); + } + + @Override + public GBDeviceEvent[] decodeResponse(byte[] responseData) { + int length = responseData.length; + if (length < 4) { + //empty message + return null; + } else { + ByteBuffer buffer = ByteBuffer.wrap(responseData, 0, length); + byte msgId = buffer.get(); + buffer.get(); + int payloadLen = buffer.getInt(); + GBDeviceEventSendBytes reply = new GBDeviceEventSendBytes(); + if (payloadLen + 6 == length) { + switch (msgId) { + case LiveviewConstants.MSG_DEVICESTATUS: + reply.encodedBytes = constructMessage(LiveviewConstants.MSG_DEVICESTATUS_ACK, new byte[]{LiveviewConstants.RESULT_OK}); + break; + case LiveviewConstants.MSG_DISPLAYPANEL_ACK: + reply.encodedBytes = encodeVibrateRequest((short) 100, (short) 200); //hack to make the notifications vibrate! + break; + default: + } + GBDeviceEventSendBytes ack = new GBDeviceEventSendBytes(); + ack.encodedBytes = constructMessage(LiveviewConstants.MSG_ACK, new byte[]{msgId}); + + return new GBDeviceEvent[]{ack, reply}; + } + } + + + return super.decodeResponse(responseData); + } + + @Override + public byte[] encodeSetTime() { + int time = (int) (Calendar.getInstance().getTimeInMillis() / 1000); + time += Calendar.getInstance().get(Calendar.ZONE_OFFSET) / 1000; + time += Calendar.getInstance().get(Calendar.DST_OFFSET) / 1000; + ByteBuffer buffer = ByteBuffer.allocate(5); + buffer.order(LiveviewConstants.BYTE_ORDER); + buffer.putInt(time); + buffer.put(LiveviewConstants.CLOCK_24H); + return constructMessage(LiveviewConstants.MSG_GETTIME_RESP, buffer.array()); + } + + @Override + public byte[] encodeNotification(NotificationSpec notificationSpec) { + String headerText; + // for SMS and EMAIL that came in though SMS or K9 receiver + if (notificationSpec.sender != null) { + headerText = notificationSpec.sender; + } else { + headerText = notificationSpec.title; + } + + String footerText = (null != notificationSpec.sourceName) ? notificationSpec.sourceName : ""; + String bodyText = (null != notificationSpec.body) ? notificationSpec.body : ""; + + byte[] headerTextArray = headerText.getBytes(LiveviewConstants.ENCODING); + byte[] footerTextArray = footerText.getBytes(LiveviewConstants.ENCODING); + byte[] bodyTextArray = bodyText.getBytes(LiveviewConstants.ENCODING); + int size = 15 + headerTextArray.length + bodyTextArray.length + footerTextArray.length; + ByteBuffer buffer = ByteBuffer.allocate(size); + buffer.put((byte) 1); + buffer.putShort((short) 0); + buffer.putShort((short) 0); + buffer.putShort((short) 0); + buffer.put((byte) 80); //should alert but it doesn't make the liveview vibrate + + buffer.put((byte) 0); //0 is for plaintext vs bitmapimage (1) strings + buffer.putShort((short) headerTextArray.length); + buffer.put(headerTextArray); + buffer.putShort((short) bodyTextArray.length); + buffer.put(bodyTextArray); + buffer.putShort((short) footerTextArray.length); + buffer.put(footerTextArray); + return constructMessage(LiveviewConstants.MSG_DISPLAYPANEL, buffer.array()); + } + + + //specific messages + + public static byte[] constructMessage(byte messageType, byte[] payload) { + ByteBuffer msgBuffer = ByteBuffer.allocate(payload.length + 6); + msgBuffer.order(LiveviewConstants.BYTE_ORDER); + msgBuffer.put(messageType); + msgBuffer.put((byte) 4); + msgBuffer.putInt(payload.length); + msgBuffer.put(payload); + return msgBuffer.array(); + } + + public byte[] encodeVibrateRequest(short delay, short time) { + ByteBuffer buffer = ByteBuffer.allocate(4); + buffer.order(LiveviewConstants.BYTE_ORDER); + buffer.putShort(delay); + buffer.putShort(time); + return constructMessage(LiveviewConstants.MSG_SETVIBRATE, buffer.array()); + } + + public byte[] encodeCapabilitiesRequest() { + byte[] version = LiveviewConstants.CLIENT_SOFTWARE_VERSION.getBytes(LiveviewConstants.ENCODING); + ByteBuffer buffer = ByteBuffer.allocate(version.length + 1); + buffer.order(LiveviewConstants.BYTE_ORDER); + buffer.put((byte) version.length); + buffer.put(version); + return constructMessage(LiveviewConstants.MSG_GETCAPS, buffer.array()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewSupport.java new file mode 100644 index 00000000..08842be7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/liveview/LiveviewSupport.java @@ -0,0 +1,106 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview; + +import android.net.Uri; + +import java.util.ArrayList; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class LiveviewSupport extends AbstractSerialDeviceSupport { + + @Override + public boolean connect() { + getDeviceIOThread().start(); + return true; + } + + @Override + protected GBDeviceProtocol createDeviceProtocol() { + return new LiveviewProtocol(getDevice()); + } + + @Override + protected GBDeviceIoThread createDeviceIOThread() { + return new LiveviewIoThread(getDevice(), getContext(), getDeviceProtocol(), LiveviewSupport.this, getBluetoothAdapter()); + } + + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + public void onInstallApp(Uri uri) { + //nothing to do ATM + } + + @Override + public void onAppConfiguration(UUID uuid, String config) { + //nothing to do ATM + } + + @Override + public void onHeartRateTest() { + //nothing to do ATM + } + + @Override + public void onSetConstantVibration(int intensity) { + //nothing to do ATM + } + + @Override + public synchronized LiveviewIoThread getDeviceIOThread() { + return (LiveviewIoThread) super.getDeviceIOThread(); + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + super.onNotification(notificationSpec); + } + + @Override + public void onSetCallState(CallSpec callSpec) { + //nothing to do ATM + } + + @Override + public void onSetMusicState(MusicStateSpec musicStateSpec) { + //nothing to do ATM + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + //nothing to do ATM + } + + + @Override + public void onSetAlarms(ArrayList alarms) { + //nothing to do ATM + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + //nothing to do ATM + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + //nothing to do ATM + } + + @Override + public void onTestNewFunction() { + //nothing to do ATM + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java index cbe8148c..cdb1e1b6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java @@ -13,7 +13,7 @@ public abstract class AbstractMi1SFirmwareInfo extends AbstractMiFirmwareInfo { @Override public boolean isGenerallyCompatibleWith(GBDevice device) { - return MiBandConst.MI_1S.equals(device.getHardwareVersion()); + return MiBandConst.MI_1S.equals(device.getModel()); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java index c1c36258..3ba149df 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/DeviceInfo.java @@ -83,7 +83,7 @@ public class DeviceInfo extends AbstractInfo { } public boolean supportsHeartrate() { - return isMiliPro() || isMili1S() || (test1AHRMode && isMili1A()); + return isMili1S() || (test1AHRMode && isMili1A()); } @Override @@ -116,10 +116,6 @@ public class DeviceInfo extends AbstractInfo { return hwVersion == 6; } - public boolean isMiliPro() { - return hwVersion == 8 || (feature == 8 && appearance == 0); - } - public String getHwVersion() { if (isMili1()) { return MiBandConst.MI_1; @@ -133,9 +129,6 @@ public class DeviceInfo extends AbstractInfo { if (isAmazFit()) { return MiBandConst.MI_AMAZFIT; } - if (isMiliPro()) { - return MiBandConst.MI_PRO; - } return "?"; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java index ce975245..e8dbf641 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1AFirmwareInfo.java @@ -31,7 +31,7 @@ public class Mi1AFirmwareInfo extends AbstractMi1FirmwareInfo { @Override public boolean isGenerallyCompatibleWith(GBDevice device) { - String hwVersion = device.getHardwareVersion(); + String hwVersion = device.getModel(); return MiBandConst.MI_1A.equals(hwVersion); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java index aa506781..37fde062 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1FirmwareInfo.java @@ -31,7 +31,7 @@ public class Mi1FirmwareInfo extends AbstractMi1FirmwareInfo { @Override public boolean isGenerallyCompatibleWith(GBDevice device) { - String hwVersion = device.getHardwareVersion(); + String hwVersion = device.getModel(); return MiBandConst.MI_1.equals(hwVersion); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java index 926afca1..8ef2e65d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java @@ -30,7 +30,7 @@ public class Mi1SFirmwareInfo extends CompositeMiFirmwareInfo { @Override public boolean isGenerallyCompatibleWith(GBDevice device) { - return MiBandConst.MI_1S.equals(device.getHardwareVersion()); + return MiBandConst.MI_1S.equals(device.getModel()); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java new file mode 100644 index 00000000..e6466471 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java @@ -0,0 +1,1277 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.support.v4.content.LocalBroadcastManager; +import android.widget.Toast; + +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.DateTimeDisplay; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +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.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.HeartRateProfile; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2NotificationStrategy; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchActivityOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.InitOperation; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PAUSE; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PROFILE; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_ORIGINAL_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PAUSE; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PROFILE; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefIntValue; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefStringValue; + +public class MiBand2Support extends AbstractBTLEDeviceSupport { + + private static final Logger LOG = LoggerFactory.getLogger(MiBand2Support.class); + private final DeviceInfoProfile deviceInfoProfile; + private final HeartRateProfile heartRateProfile; + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String s = intent.getAction(); + if (s.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { + handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); + } + } + }; + + private boolean needsAuth; + private volatile boolean telephoneRinging; + private volatile boolean isLocatingDevice; + + private DeviceInfo mDeviceInfo; + + private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + private RealtimeSamplesSupport realtimeSamplesSupport; + + public MiBand2Support() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(GattService.UUID_SERVICE_HEART_RATE); + addSupportedService(GattService.UUID_SERVICE_IMMEDIATE_ALERT); + addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); + addSupportedService(GattService.UUID_SERVICE_ALERT_NOTIFICATION); + + addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); + addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE); + + deviceInfoProfile = new DeviceInfoProfile<>(this); + addSupportedProfile(deviceInfoProfile); + heartRateProfile = new HeartRateProfile<>(this); + addSupportedProfile(heartRateProfile); + + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(DeviceInfoProfile.ACTION_DEVICE_INFO); + intentFilter.addAction(DeviceService.ACTION_MIBAND2_AUTH); + broadcastManager.registerReceiver(mReceiver, intentFilter); + } + + @Override + public void dispose() { + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + broadcastManager.unregisterReceiver(mReceiver); + super.dispose(); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + try { + boolean authenticate = needsAuth; + needsAuth = false; + new InitOperation(authenticate, this, builder).perform(); + } catch (IOException e) { + GB.toast(getContext(), "Initializing Mi Band 2 failed", Toast.LENGTH_SHORT, GB.ERROR, e); + } + +// builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZING, getContext())); +// enableNotifications(builder, true) +// .setLowLatency(builder) +// .readDate(builder) // without reading the data, we get sporadic connection problems, especially directly after turning on BT +// this is apparently not needed anymore, and actually causes problems when bonding is not used/does not work +// so we simply not use the UUID_PAIR characteristic. +// .pair(builder) + //.requestDeviceInfo(builder) + //.requestBatteryInfo(builder); +// .sendUserInfo(builder) +// .checkAuthenticationNeeded(builder, getDevice()) +// .setWearLocation(builder) +// .setHeartrateSleepSupport(builder) +// .setFitnessGoal(builder) +// .enableFurtherNotifications(builder, true) +// .setCurrentTime(builder) +// .requestBatteryInfo(builder) +// .setHighLatency(builder) +// .setInitialized(builder); + return builder; + } + + public byte[] getTimeBytes(Calendar calendar, TimeUnit precision) { + byte[] bytes; + if (precision == TimeUnit.MINUTES) { + bytes = BLETypeConversions.shortCalendarToRawBytes(calendar, true); + } else if (precision == TimeUnit.SECONDS) { + bytes = BLETypeConversions.calendarToRawBytes(calendar, true); + } else { + throw new IllegalArgumentException("Unsupported precision, only MINUTES and SECONDS are supported till now"); + } + byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(calendar.getTimeZone()) }; // 0 = adjust reason bitflags? or DST offset?? , timezone +// byte[] tail = new byte[] { 0x2 }; // reason + byte[] all = BLETypeConversions.join(bytes, tail); + return all; + } + + public Calendar fromTimeBytes(byte[] bytes) { + GregorianCalendar timestamp = BLETypeConversions.rawBytesToCalendar(bytes, true); + return timestamp; + } + + public MiBand2Support setCurrentTimeWithService(TransactionBuilder builder) { + GregorianCalendar now = BLETypeConversions.createCalendar(); + byte[] bytes = getTimeBytes(now, TimeUnit.SECONDS); + builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), bytes); + +// byte[] localtime = BLETypeConversions.calendarToLocalTimeBytes(now); +// builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_LOCAL_TIME_INFORMATION), localtime); +// builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), new byte[] {0x2, 0x00}); +// builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0), new byte[] {0x03,0x00,(byte)0x8e,(byte)0xce,0x5a,0x09,(byte)0xb3,(byte)0xd8,0x55,0x57,0x10,0x2a,(byte)0xed,0x7d,0x6b,0x78,(byte)0xc5,(byte)0xd2}); + return this; + } + + private MiBand2Support readDate(TransactionBuilder builder) { + // NAVL +// builder.read(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_DATE_TIME)); + // TODO: handle result + builder.read(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME)); + return this; + } + + // NAVL + public MiBand2Support setLowLatency(TransactionBuilder builder) { +// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getLowLatency()); + return this; + } + // NAVL + public MiBand2Support setHighLatency(TransactionBuilder builder) { +// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getHighLatency()); + return this; + } + + private MiBand2Support checkAuthenticationNeeded(TransactionBuilder builder, GBDevice device) { + builder.add(new CheckAuthenticationNeededAction(device)); + return this; + } + + /** + * Last action of initialization sequence. Sets the device to initialized. + * It is only invoked if all other actions were successfully run, so the device + * must be initialized, then. + * + * @param builder + */ + public void setInitialized(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZED, getContext())); + } + + // MB2: AVL + // TODO: tear down the notifications on quit + public MiBand2Support enableNotifications(TransactionBuilder builder, boolean enable) { + builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_NOTIFICATION), enable); + builder.notify(getCharacteristic(GattService.UUID_SERVICE_CURRENT_TIME), enable); + // Notify CHARACTERISTIC9 to receive random auth code + builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), enable); + return this; + } + + public MiBand2Support enableFurtherNotifications(TransactionBuilder builder, boolean enable) { +// builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable) +// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable) +// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_BATTERY), enable) +// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable); + builder.notify(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), enable); + BluetoothGattCharacteristic heartrateCharacteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT); + if (heartrateCharacteristic != null) { + builder.notify(heartrateCharacteristic, enable); + } + + return this; + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void pair() { + needsAuth = true; + for (int i = 0; i < 5; i++) { + if (connect()) { + return; + } + } + } + + public DeviceInfo getDeviceInfo() { + return mDeviceInfo; + } + + private MiBand2Support sendDefaultNotification(TransactionBuilder builder, short repeat, BtLEAction extraAction) { + LOG.info("Sending notification to MiBand: (" + repeat + " times)"); + NotificationStrategy strategy = getNotificationStrategy(); + for (short i = 0; i < repeat; i++) { + strategy.sendDefaultNotification(builder, extraAction); + } + return this; + } + + /** + * Adds a custom notification to the given transaction builder + * + * @param vibrationProfile specifies how and how often the Band shall vibrate. + * @param flashTimes + * @param flashColour + * @param originalColour + * @param flashDuration + * @param extraAction an extra action to be executed after every vibration and flash sequence. Allows to abort the repetition, for example. + * @param builder + */ + private MiBand2Support sendCustomNotification(VibrationProfile vibrationProfile, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) { + getNotificationStrategy().sendCustomNotification(vibrationProfile, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder); + LOG.info("Sending notification to MiBand"); + return this; + } + + private NotificationStrategy getNotificationStrategy() { + return new Mi2NotificationStrategy(this); + } + + static final byte[] reboot = new byte[]{MiBandService.COMMAND_REBOOT}; + + static final byte[] startHeartMeasurementManual = new byte[]{0x15, MiBandService.COMMAND_SET_HR_MANUAL, 1}; + static final byte[] stopHeartMeasurementManual = new byte[]{0x15, MiBandService.COMMAND_SET_HR_MANUAL, 0}; + static final byte[] startHeartMeasurementContinuous = new byte[]{0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 1}; + static final byte[] stopHeartMeasurementContinuous = new byte[]{0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 0}; + static final byte[] startHeartMeasurementSleep = new byte[]{0x15, MiBandService.COMMAND_SET_HR_SLEEP, 1}; + static final byte[] stopHeartMeasurementSleep = new byte[]{0x15, MiBandService.COMMAND_SET_HR_SLEEP, 0}; + + static final byte[] startRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 1}; + static final byte[] stopRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 0}; + + /** + * Part of device initialization process. Do not call manually. + * + * @param builder + * @return + */ + private MiBand2Support sendUserInfo(TransactionBuilder builder) { + LOG.debug("Writing User Info!"); + // Use a custom action instead of just builder.write() because mDeviceInfo + // is set by handleDeviceInfo *after* this action is created. + builder.add(new BtLEAction(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_USER_INFO)) { + @Override + public boolean expectsResult() { + return true; + } + + @Override + public boolean run(BluetoothGatt gatt) { + // at this point, mDeviceInfo should be set + return new WriteAction(getCharacteristic(), + MiBandCoordinator.getAnyUserInfo(getDevice().getAddress()).getData(mDeviceInfo) + ).run(gatt); + } + }); + return this; + } + + public MiBand2Support requestDeviceInfo(TransactionBuilder builder) { + LOG.debug("Requesting Device Info!"); + deviceInfoProfile.requestDeviceInfo(builder); + return this; + } + + /* private MiBandSupport requestHRInfo(TransactionBuilder builder) { + LOG.debug("Requesting HR Info!"); + BluetoothGattCharacteristic HRInfo = getCharacteristic(MiBandService.UUID_CHAR_HEART_RATE_MEASUREMENT); + builder.read(HRInfo); + BluetoothGattCharacteristic HR_Point = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT); + builder.read(HR_Point); + return this; + } + *//** + * Part of HR test. Do not call manually. + * + * @param transaction + * @return + *//* + private MiBandSupport heartrate(TransactionBuilder transaction) { + LOG.info("Attempting to read HR ..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHAR_HEART_RATE_MEASUREMENT); + if (characteristic != null) { + transaction.write(characteristic, new byte[]{MiBandService.COMMAND_SET__HR_CONTINUOUS}); + } else { + LOG.info("Unable to read HR from MI device -- characteristic not available"); + } + return this; + }*/ + + /** + * Part of device initialization process. Do not call manually. + * + * @param transaction + * @return + */ + + private MiBand2Support setFitnessGoal(TransactionBuilder transaction) { + LOG.info("Attempting to set Fitness Goal..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC8); + if (characteristic != null) { + int fitnessGoal = MiBandCoordinator.getFitnessGoal(getDevice().getAddress()); + byte[] bytes = ArrayUtils.addAll( + MiBand2Service.COMMAND_SET_FITNESS_GOAL_START, + BLETypeConversions.fromUint16(fitnessGoal)); + bytes = ArrayUtils.addAll(bytes, + MiBand2Service.COMMAND_SET_FITNESS_GOAL_END); + transaction.write(characteristic, bytes); + } else { + LOG.info("Unable to set Fitness Goal"); + } + return this; + } + + /** + * Part of device initialization process. Do not call manually. + * + * @param builder + * @return + */ + private MiBand2Support setWearLocation(TransactionBuilder builder) { + LOG.info("Attempting to set wear location..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC8); + if (characteristic != null) { + builder.notify(characteristic, true); + int location = MiBandCoordinator.getWearLocation(getDevice().getAddress()); + switch (location) { + case 0: // left hand + builder.write(characteristic, MiBand2Service.WEAR_LOCATION_LEFT_WRIST); + break; + case 1: // right hand + builder.write(characteristic, MiBand2Service.WEAR_LOCATION_RIGHT_WRIST); + break; + } + builder.notify(characteristic, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed. + } + return this; + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + try { + TransactionBuilder builder = performInitialized("enable heart rate sleep support: " + enable); + setHeartrateSleepSupport(builder); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error toggling heart rate sleep support: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + // not supported + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + // not supported + } + + /** + * Part of device initialization process. Do not call manually. + * + * @param builder + */ + private MiBand2Support setHeartrateSleepSupport(TransactionBuilder builder) { + BluetoothGattCharacteristic characteristicHRControlPoint = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT); + final boolean enableHrSleepSupport = MiBandCoordinator.getHeartrateSleepSupport(getDevice().getAddress()); + if (characteristicHRControlPoint != null) { + builder.notify(characteristicHRControlPoint, true); + if (enableHrSleepSupport) { + LOG.info("Enabling heartrate sleep support..."); + builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_ENABLE_HR_SLEEP_MEASUREMENT); + } else { + LOG.info("Disabling heartrate sleep support..."); + builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_DISABLE_HR_SLEEP_MEASUREMENT); + } + builder.notify(characteristicHRControlPoint, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed. + } + return this; + } + + private void performDefaultNotification(String task, short repeat, BtLEAction extraAction) { + try { + TransactionBuilder builder = performInitialized(task); + sendDefaultNotification(builder, repeat, extraAction); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send notification to MI device", ex); + } + } + + private void performPreferredNotification(String task, String notificationOrigin, int alertLevel, BtLEAction extraAction) { + try { + TransactionBuilder builder = performInitialized(task); + Prefs prefs = GBApplication.getPrefs(); + int vibrateDuration = getPreferredVibrateDuration(notificationOrigin, prefs); + int vibratePause = getPreferredVibratePause(notificationOrigin, prefs); + short vibrateTimes = getPreferredVibrateCount(notificationOrigin, prefs); + VibrationProfile profile = getPreferredVibrateProfile(notificationOrigin, prefs, vibrateTimes); + profile.setAlertLevel(alertLevel); + + int flashTimes = getPreferredFlashCount(notificationOrigin, prefs); + int flashColour = getPreferredFlashColour(notificationOrigin, prefs); + int originalColour = getPreferredOriginalColour(notificationOrigin, prefs); + int flashDuration = getPreferredFlashDuration(notificationOrigin, prefs); + + sendCustomNotification(profile, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder); +// sendCustomNotification(vibrateDuration, vibrateTimes, vibratePause, flashTimes, flashColour, originalColour, flashDuration, builder); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send notification to MI device", ex); + } + } + + private int getPreferredFlashDuration(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(FLASH_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_DURATION); + } + + private int getPreferredOriginalColour(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(FLASH_ORIGINAL_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR); + } + + private int getPreferredFlashColour(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(FLASH_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COLOUR); + } + + private int getPreferredFlashCount(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(FLASH_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COUNT); + } + + private int getPreferredVibratePause(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(VIBRATION_PAUSE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PAUSE); + } + + private short getPreferredVibrateCount(String notificationOrigin, Prefs prefs) { + return (short) Math.min(Short.MAX_VALUE, getNotificationPrefIntValue(VIBRATION_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_COUNT)); + } + + private int getPreferredVibrateDuration(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(VIBRATION_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_DURATION); + } + + private VibrationProfile getPreferredVibrateProfile(String notificationOrigin, Prefs prefs, short repeat) { + String profileId = getNotificationPrefStringValue(VIBRATION_PROFILE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PROFILE); + return VibrationProfile.getProfile(profileId, repeat); + } + + @Override + public void onSetAlarms(ArrayList alarms) { + try { + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3); + TransactionBuilder builder = performInitialized("Set alarm"); + boolean anyAlarmEnabled = false; + for (Alarm alarm : alarms) { + anyAlarmEnabled |= alarm.isEnabled(); + queueAlarm(alarm, builder, characteristic); + } + builder.queue(getQueue()); + if (anyAlarmEnabled) { + GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_ok), Toast.LENGTH_SHORT, GB.INFO); + } else { + GB.toast(getContext(), getContext().getString(R.string.user_feedback_all_alarms_disabled), Toast.LENGTH_SHORT, GB.INFO); + } + } catch (IOException ex) { + GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_failed), Toast.LENGTH_LONG, GB.ERROR, ex); + } + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + int alertLevel = MiBand2Service.ALERT_LEVEL_MESSAGE; + if (notificationSpec.type == NotificationType.UNKNOWN) { + alertLevel = MiBand2Service.ALERT_LEVEL_VIBRATE_ONLY; + } + String origin = notificationSpec.type.getGenericType(); + performPreferredNotification(origin + " received", origin, alertLevel, null); + } + + @Override + public void onSetTime() { + try { + TransactionBuilder builder = performInitialized("Set date and time"); + setCurrentTimeWithService(builder); + //TODO: once we have a common strategy for sending events (e.g. EventHandler), remove this call from here. Meanwhile it does no harm. + sendCalendarEvents(builder); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to set time on MI device", ex); + } + } + + @Override + public void onSetCallState(CallSpec callSpec) { + if (callSpec.command == CallSpec.CALL_INCOMING) { + telephoneRinging = true; + AbortTransactionAction abortAction = new AbortTransactionAction() { + @Override + protected boolean shouldAbort() { + return !isTelephoneRinging(); + } + + @Override + public boolean run(BluetoothGatt gatt) { + if (!super.run(gatt)) { + // send a signal to stop the vibration + BluetoothGattCharacteristic characteristic = MiBand2Support.this.getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL); + characteristic.setValue(new byte[] {MiBand2Service.ALERT_LEVEL_NONE}); + gatt.writeCharacteristic(characteristic); + return false; + } + return true; + } + }; + performPreferredNotification("incoming call", MiBandConst.ORIGIN_INCOMING_CALL, MiBand2Service.ALERT_LEVEL_PHONE_CALL, abortAction); + } else if ((callSpec.command == CallSpec.CALL_START) || (callSpec.command == CallSpec.CALL_END)) { + telephoneRinging = false; + } + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + } + + private boolean isTelephoneRinging() { + // don't synchronize, this is not really important + return telephoneRinging; + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + // not supported + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + // not supported + } + + @Override + public void onReboot() { + try { + TransactionBuilder builder = performInitialized("Reboot"); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), reboot); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to reboot MI", ex); + } + } + + @Override + public void onHeartRateTest() { + try { + TransactionBuilder builder = performInitialized("HeartRateTest"); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementManual); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to read HearRate with MI2", ex); + } + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + try { + TransactionBuilder builder = performInitialized("Enable realtime heart rateM measurement"); + if (enable) { + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementContinuous); + } else { + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous); + } + builder.queue(getQueue()); + enableRealtimeSamplesTimer(enable); + } catch (IOException ex) { + LOG.error("Unable to enable realtime heart rate measurement in MI1S", ex); + } + } + + @Override + public void onFindDevice(boolean start) { + isLocatingDevice = start; + + if (start) { + AbortTransactionAction abortAction = new AbortTransactionAction() { + @Override + protected boolean shouldAbort() { + return !isLocatingDevice; + } + }; + performDefaultNotification("locating device", (short) 255, abortAction); + } + } + + @Override + public void onSetConstantVibration(int intensity) { + + } + + @Override + public void onFetchActivityData() { + try { + new FetchActivityOperation(this).perform(); + } catch (IOException ex) { + LOG.error("Unable to fetch MI activity data", ex); + } + } + + @Override + public void onEnableRealtimeSteps(boolean enable) { +// try { +// BluetoothGattCharacteristic controlPoint = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); +// if (enable) { +// TransactionBuilder builder = performInitialized("Read realtime steps"); +// builder.read(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS)).queue(getQueue()); +// } +// performInitialized(enable ? "Enabling realtime steps notifications" : "Disabling realtime steps notifications") +// .write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), enable ? getLowLatency() : getHighLatency()) +// .write(controlPoint, enable ? startRealTimeStepsNotifications : stopRealTimeStepsNotifications).queue(getQueue()); +// enableRealtimeSamplesTimer(enable); +// } catch (IOException e) { +// LOG.error("Unable to change realtime steps notification to: " + enable, e); +// } + } + + private byte[] getHighLatency() { + int minConnectionInterval = 460; + int maxConnectionInterval = 500; + int latency = 0; + int timeout = 500; + int advertisementInterval = 0; + + return getLatency(minConnectionInterval, maxConnectionInterval, latency, timeout, advertisementInterval); + } + + private byte[] getLatency(int minConnectionInterval, int maxConnectionInterval, int latency, int timeout, int advertisementInterval) { + byte result[] = new byte[12]; + result[0] = (byte) (minConnectionInterval & 0xff); + result[1] = (byte) (0xff & minConnectionInterval >> 8); + result[2] = (byte) (maxConnectionInterval & 0xff); + result[3] = (byte) (0xff & maxConnectionInterval >> 8); + result[4] = (byte) (latency & 0xff); + result[5] = (byte) (0xff & latency >> 8); + result[6] = (byte) (timeout & 0xff); + result[7] = (byte) (0xff & timeout >> 8); + result[8] = 0; + result[9] = 0; + result[10] = (byte) (advertisementInterval & 0xff); + result[11] = (byte) (0xff & advertisementInterval >> 8); + + return result; + } + + private byte[] getLowLatency() { + int minConnectionInterval = 39; + int maxConnectionInterval = 49; + int latency = 0; + int timeout = 500; + int advertisementInterval = 0; + + return getLatency(minConnectionInterval, maxConnectionInterval, latency, timeout, advertisementInterval); + } + + @Override + public void onInstallApp(Uri uri) { +// TODO: onInstallApp (firmware update) +// try { +// new UpdateFirmwareOperation(uri, this).perform(); +// } catch (IOException ex) { +// GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); +// } + } + + @Override + public void onAppInfoReq() { + // not supported + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + // not supported + } + + @Override + public void onAppDelete(UUID uuid) { + // not supported + } + + @Override + public void onAppConfiguration(UUID uuid, String config) { + // not supported + } + + @Override + public void onAppReorder(UUID[] uuids) { + // not supported + } + + @Override + public void onScreenshotReq() { + // not supported + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + super.onCharacteristicChanged(gatt, characteristic); + + UUID characteristicUUID = characteristic.getUuid(); + if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) { + handleBatteryInfo(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS); + return true; + } else if (MiBandService.UUID_CHARACTERISTIC_NOTIFICATION.equals(characteristicUUID)) { + handleNotificationNotif(characteristic.getValue()); + return true; + } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) { + handleRealtimeSteps(characteristic.getValue()); + return true; + } else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { + handleHeartrate(characteristic.getValue()); + return true; +// } else if (MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0.equals(characteristicUUID)) { +// handleUnknownCharacteristic(characteristic.getValue()); +// return true; + } else if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { + LOG.info("AUTHENTICATION?? " + characteristicUUID); + logMessageContent(characteristic.getValue()); + return true; + } else { + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + logMessageContent(characteristic.getValue()); + } + return false; + } + + private void handleUnknownCharacteristic(byte[] value) { + + } + + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + super.onCharacteristicRead(gatt, characteristic, status); + + UUID characteristicUUID = characteristic.getUuid(); + if (GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME.equals(characteristicUUID)) { + handleDeviceName(characteristic.getValue(), status); + return true; + } else if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) { + handleBatteryInfo(characteristic.getValue(), status); + return true; + } else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { + logHeartrate(characteristic.getValue(), status); + return true; + } else if (MiBandService.UUID_CHARACTERISTIC_DATE_TIME.equals(characteristicUUID)) { + logDate(characteristic.getValue(), status); + return true; + } else { + LOG.info("Unhandled characteristic read: " + characteristicUUID); + logMessageContent(characteristic.getValue()); + } + return false; + } + + @Override + public boolean onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + UUID characteristicUUID = characteristic.getUuid(); + if (MiBandService.UUID_CHARACTERISTIC_PAIR.equals(characteristicUUID)) { + handlePairResult(characteristic.getValue(), status); + return true; + } else if (MiBandService.UUID_CHARACTERISTIC_USER_INFO.equals(characteristicUUID)) { + handleUserInfoResult(characteristic.getValue(), status); + return true; + } else if (MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT.equals(characteristicUUID)) { + handleControlPointResult(characteristic.getValue(), status); + return true; + } else if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { + LOG.info("KEY AES SEND"); + logMessageContent(characteristic.getValue()); + return true; + } + return false; + } + + public void logDate(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + GregorianCalendar calendar = MiBandDateConverter.rawBytesToCalendar(value); + LOG.info("Got Mi Band Date: " + DateTimeUtils.formatDateTime(calendar.getTime())); + } else { + logMessageContent(value); + } + } + + public void logHeartrate(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS && value != null) { + LOG.info("Got heartrate:"); + if (value.length == 2 && value[0] == 0) { + int hrValue = (value[1] & 0xff); + GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO); + } + return; + } + logMessageContent(value); + } + + private void handleHeartrate(byte[] value) { + if (value.length == 2 && value[0] == 0) { + int hrValue = (value[1] & 0xff); + if (LOG.isDebugEnabled()) { + LOG.debug("heart rate: " + hrValue); + } + RealtimeSamplesSupport realtimeSamplesSupport = getRealtimeSamplesSupport(); + realtimeSamplesSupport.setHeartrateBpm(hrValue); + if (!realtimeSamplesSupport.isRunning()) { + // single shot measurement, manually invoke storage and result publishing + realtimeSamplesSupport.triggerCurrentSample(); + } + } + } + + private void handleRealtimeSteps(byte[] value) { + int steps = 0xff & value[0] | (0xff & value[1]) << 8; + if (LOG.isDebugEnabled()) { + LOG.debug("realtime steps: " + steps); + } + getRealtimeSamplesSupport().setSteps(steps); + } + + private void enableRealtimeSamplesTimer(boolean enable) { + if (enable) { + getRealtimeSamplesSupport().start(); + } else { + if (realtimeSamplesSupport != null) { + realtimeSamplesSupport.stop(); + } + } + } + + public MiBandActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) { + MiBandActivitySample sample = new MiBandActivitySample(); + sample.setDevice(device); + sample.setUser(user); + sample.setTimestamp(timestampInSeconds); + sample.setProvider(provider); + + return sample; + } + + private RealtimeSamplesSupport getRealtimeSamplesSupport() { + if (realtimeSamplesSupport == null) { + realtimeSamplesSupport = new RealtimeSamplesSupport(1000, 1000) { + @Override + public void doCurrentSample() { + + try (DBHandler handler = GBApplication.acquireDB()) { + DaoSession session = handler.getDaoSession(); + + Device device = DBHelper.getDevice(getDevice(), session); + User user = DBHelper.getUser(session); + int ts = (int) (System.currentTimeMillis() / 1000); + MiBand2SampleProvider provider = new MiBand2SampleProvider(gbDevice, session); + MiBandActivitySample sample = createActivitySample(device, user, ts, provider); + sample.setHeartRate(getHeartrateBpm()); + sample.setSteps(getSteps()); + sample.setRawIntensity(ActivitySample.NOT_MEASURED); + sample.setRawKind(MiBand2SampleProvider.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that? + + // TODO: remove this once fully ported to REALTIME_SAMPLES + if (sample.getSteps() != ActivitySample.NOT_MEASURED) { + Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS) + .putExtra(DeviceService.EXTRA_REALTIME_STEPS, sample.getSteps()) + .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + if (sample.getHeartRate() != ActivitySample.NOT_MEASURED) { + Intent intent = new Intent(DeviceService.ACTION_HEARTRATE_MEASUREMENT) + .putExtra(DeviceService.EXTRA_HEART_RATE_VALUE, sample.getHeartRate()) + .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + +// Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) +// .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample); +// LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + + LOG.debug("Storing realtime sample: " + sample); + provider.addGBActivitySample(sample); + } catch (Exception e) { + LOG.warn("Unable to acquire db for saving realtime samples", e); + } + } + }; + } + return realtimeSamplesSupport; + } + + /** + * React to unsolicited messages sent by the Mi Band to the MiBandService.UUID_CHARACTERISTIC_NOTIFICATION + * characteristic, + * These messages appear to be always 1 byte long, with values that are listed in MiBandService. + * It is not excluded that there are further values which are still unknown. + *

+ * Upon receiving known values that request further action by GB, the appropriate method is called. + * + * @param value + */ + private void handleNotificationNotif(byte[] value) { + if (value.length != 1) { + LOG.error("Notifications should be 1 byte long."); + LOG.info("RECEIVED DATA WITH LENGTH: " + value.length); + for (byte b : value) { + LOG.warn("DATA: " + String.format("0x%2x", b)); + } + return; + } + switch (value[0]) { + case MiBandService.NOTIFY_AUTHENTICATION_FAILED: + // we get first FAILED, then NOTIFY_STATUS_MOTOR_AUTH (0x13) + // which means, we need to authenticate by tapping + getDevice().setState(State.AUTHENTICATION_REQUIRED); + getDevice().sendDeviceUpdateIntent(getContext()); + GB.toast(getContext(), "Band needs pairing", Toast.LENGTH_LONG, GB.ERROR); + break; + case MiBandService.NOTIFY_AUTHENTICATION_SUCCESS: // fall through -- not sure which one we get + case MiBandService.NOTIFY_RESET_AUTHENTICATION_SUCCESS: // for Mi 1A + case MiBandService.NOTIFY_STATUS_MOTOR_AUTH_SUCCESS: + LOG.info("Band successfully authenticated"); + // maybe we can perform the rest of the initialization from here + doInitialize(); + break; + + case MiBandService.NOTIFY_STATUS_MOTOR_AUTH: + LOG.info("Band needs authentication (MOTOR_AUTH)"); + getDevice().setState(State.AUTHENTICATING); + getDevice().sendDeviceUpdateIntent(getContext()); + break; + + case MiBandService.NOTIFY_SET_LATENCY_SUCCESS: + LOG.info("Setting latency succeeded."); + break; + default: + for (byte b : value) { + LOG.warn("DATA: " + String.format("0x%2x", b)); + } + } + } + + private void doInitialize() { + try { + TransactionBuilder builder = performInitialized("just initializing after authentication"); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to initialize device after authentication", ex); + } + } + + private void handleDeviceName(byte[] value, int status) { +// if (status == BluetoothGatt.GATT_SUCCESS) { +// versionCmd.hwVersion = new String(value); +// handleGBDeviceEvent(versionCmd); +// } + } + + /** + * Convert an alarm from the GB internal structure to a Mi Band message and put on the specified + * builder queue as a write message for the passed characteristic + * + * @param alarm + * @param builder + * @param characteristic + */ + private void queueAlarm(Alarm alarm, TransactionBuilder builder, BluetoothGattCharacteristic characteristic) { + Calendar calendar = alarm.getAlarmCal(); + + int maxAlarms = 5; // arbitrary at the moment... + if (alarm.getIndex() >= maxAlarms) { + if (alarm.isEnabled()) { + GB.toast(getContext(), "Only 5 alarms are currently supported.", Toast.LENGTH_LONG, GB.WARN); + } + return; + } + + int base = 0; + if (alarm.isEnabled()) { + base = 128; + } + int daysMask = alarm.getRepetitionMask(); + if (!alarm.isRepetitive()) { + daysMask = 128; + } + byte[] alarmMessage = new byte[] { + (byte) 0x2, // TODO what is this? + (byte) (base + alarm.getIndex()), // 128 is the base, alarm slot is added + (byte) calendar.get(Calendar.HOUR_OF_DAY), + (byte) calendar.get(Calendar.MINUTE), + (byte) daysMask, + }; + builder.write(characteristic, alarmMessage); + // TODO: react on 0x10, 0x02, 0x01 on notification (success) + } + + private void handleControlPointResult(byte[] value, int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + LOG.warn("Could not write to the control point."); + } + LOG.info("handleControlPoint write status:" + status + "; length: " + (value != null ? value.length : "(null)")); + + if (value != null) { + for (byte b : value) { + LOG.info("handleControlPoint WROTE DATA:" + String.format("0x%8x", b)); + } + } else { + LOG.warn("handleControlPoint WROTE null"); + } + } + + private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { +// if (getDeviceInfo().supportsHeartrate()) { +// getDevice().addDeviceInfo(new GenericItem( +// getContext().getString(R.string.DEVINFO_HR_VER), +// info.getSoftwareRevision())); +// } + LOG.warn("Device info: " + info); + versionCmd.hwVersion = info.getHardwareRevision(); +// versionCmd.fwVersion = info.getFirmwareRevision(); // always null + versionCmd.fwVersion = info.getSoftwareRevision(); + handleGBDeviceEvent(versionCmd); + } + + private void handleBatteryInfo(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + BatteryInfo info = new BatteryInfo(value); + batteryCmd.level = ((short) info.getLevelInPercent()); + batteryCmd.state = info.getState(); + batteryCmd.lastChargeTime = info.getLastChargeTime(); + batteryCmd.numCharges = info.getNumCharges(); + handleGBDeviceEvent(batteryCmd); + } + } + + private void handleUserInfoResult(byte[] value, int status) { + // successfully transferred user info means we're initialized +// commented out, because we have SetDeviceStateAction which sets initialized +// state on every successful initialization. +// if (status == BluetoothGatt.GATT_SUCCESS) { +// setConnectionState(State.INITIALIZED); +// } + } + + private void setConnectionState(State newState) { + getDevice().setState(newState); + getDevice().sendDeviceUpdateIntent(getContext()); + } + + private void handlePairResult(byte[] pairResult, int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + LOG.info("Pairing MI device failed: " + status); + return; + } + + String value = null; + if (pairResult != null) { + if (pairResult.length == 1) { + try { + if (pairResult[0] == 2) { + LOG.info("Successfully paired MI device"); + return; + } + } catch (Exception ex) { + LOG.warn("Error identifying pairing result", ex); + return; + } + } + value = Arrays.toString(pairResult); + } + LOG.info("MI Band pairing result: " + value); + } + + /** + * Fetch the events from the android device calendars and set the alarms on the miband. + * @param builder + */ + private MiBand2Support sendCalendarEvents(TransactionBuilder builder) { + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3); + + Prefs prefs = GBApplication.getPrefs(); + int availableSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0); + + if (availableSlots > 0) { + CalendarEvents upcomingEvents = new CalendarEvents(); + List mEvents = upcomingEvents.getCalendarEventList(getContext()); + + int iteration = 0; + for (CalendarEvents.CalendarEvent mEvt : mEvents) { + if (iteration >= availableSlots || iteration > 2) { + break; + } + int slotToUse = 2 - iteration; + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(mEvt.getBegin()); + Alarm alarm = GBAlarm.createSingleShot(slotToUse, false, calendar); + queueAlarm(alarm, builder, characteristic); + iteration++; + } + builder.queue(getQueue()); + } + return this; + } + + @Override + public void onSendConfiguration(String config) { + TransactionBuilder builder = null; + try { + builder = performInitialized("Sending configuration for option: " + config); + switch (config) { + case MiBandConst.PREF_MI2_DATEFORMAT: + setDateDisplay(builder); + break; + case MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT: + setActivateDisplayOnLiftWrist(builder); + break; + case MiBandConst.PREF_MIBAND_FITNESS_GOAL: + setFitnessGoal(builder); + break; + } + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e); + } + } + + @Override + public void onTestNewFunction() { + } + + private MiBand2Support setDateDisplay(TransactionBuilder builder) { + DateTimeDisplay dateTimeDisplay = MiBand2Coordinator.getDateDisplay(getContext()); + LOG.info("Setting date display to " + dateTimeDisplay); + switch (dateTimeDisplay) { + case TIME: + builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.DATEFORMAT_TIME); + break; + case DATE_TIME: + builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.DATEFORMAT_DATE_TIME); + break; + } + return this; + } + + private MiBand2Support setActivateDisplayOnLiftWrist(TransactionBuilder builder) { + boolean enable = MiBand2Coordinator.getActivateDisplayOnLiftWrist(); + LOG.info("Setting activate display on lift wrist to " + enable); + if (enable) { + builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST); + } else { + builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST); + } + return this; + } + + public void phase2Initialize(TransactionBuilder builder) { + LOG.info("phase2Initialize..."); + enableFurtherNotifications(builder, true); + setDateDisplay(builder); + setWearLocation(builder); + setFitnessGoal(builder); + setActivateDisplayOnLiftWrist(builder); + setHeartrateSleepSupport(builder); + + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java index 165e72e5..45fcc9fd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java @@ -21,22 +21,32 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; -import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; @@ -67,10 +77,6 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FL import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COUNT; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_DURATION; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_ORIGINAL_COLOUR; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_GENERIC; -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.VIBRATION_COUNT; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_DURATION; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PAUSE; @@ -88,13 +94,16 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { public static final boolean MI_1A_HR_FW_UPDATE_TEST_MODE_ENABLED = false; private volatile boolean telephoneRinging; private volatile boolean isLocatingDevice; + private volatile boolean isReadingSensorData; private DeviceInfo mDeviceInfo; private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + private RealtimeSamplesSupport realtimeSamplesSupport; public MiBandSupport() { + super(LOG); addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); @@ -243,6 +252,9 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { static final byte[] startRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 1}; static final byte[] stopRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 0}; + private static final byte[] startSensorRead = new byte[]{MiBandService.COMMAND_GET_SENSOR_DATA, 1}; + private static final byte[] stopSensorRead = new byte[]{MiBandService.COMMAND_GET_SENSOR_DATA, 0}; + /** * Part of device initialization process. Do not call manually. * @@ -318,12 +330,21 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { * @return */ private MiBandSupport pair(TransactionBuilder transaction) { - LOG.info("Attempting to pair MI device..."); - BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_PAIR); - if (characteristic != null) { - transaction.write(characteristic, new byte[]{2}); - } else { - LOG.info("Unable to pair MI device -- characteristic not available"); +// this is apparently only needed to get a more strict bond between mobile and mi band, +// e.g. such that Mi Fit and Gadgetbridge can coexist without needing to re-pair (with +// full device-data-reset). +// Unfortunately this extra pairing causes problems when bonding is not used/does not work +// so we only do this when configured to keep data on the device + + Prefs prefs = GBApplication.getPrefs(); + if (prefs.getBoolean(MiBandConst.PREF_MIBAND_DONT_ACK_TRANSFER, false)) { + LOG.info("Attempting to pair MI device..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_PAIR); + if (characteristic != null) { + transaction.write(characteristic, new byte[]{2}); + } else { + LOG.info("Unable to pair MI device -- characteristic not available"); + } } return this; } @@ -453,7 +474,9 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { int originalColour = getPreferredOriginalColour(notificationOrigin, prefs); int flashDuration = getPreferredFlashDuration(notificationOrigin, prefs); +// setLowLatency(builder); sendCustomNotification(profile, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder); +// setHighLatency(builder); // sendCustomNotification(vibrateDuration, vibrateTimes, vibratePause, flashTimes, flashColour, originalColour, flashDuration, builder); builder.queue(getQueue()); } catch (IOException ex) { @@ -517,20 +540,8 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { @Override public void onNotification(NotificationSpec notificationSpec) { - // FIXME: these ORIGIN contants do not really make sense anymore - switch (notificationSpec.type) { - case SMS: - performPreferredNotification("sms received", ORIGIN_SMS, null); - break; - case EMAIL: - performPreferredNotification("email received", ORIGIN_K9MAIL, null); - break; - case CHAT: - performPreferredNotification("chat message received", ORIGIN_PEBBLEMSG, null); - break; - default: - performPreferredNotification("generic notification received", ORIGIN_GENERIC, null); - } + String origin = notificationSpec.type.getGenericType(); + performPreferredNotification(origin + " received", origin, null); } @Override @@ -595,6 +606,10 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } } + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + } + private boolean isTelephoneRinging() { // don't synchronize, this is not really important return telephoneRinging; @@ -650,6 +665,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous); } builder.queue(getQueue()); + enableRealtimeSamplesTimer(enable); } catch (IOException ex) { LOG.error("Unable to enable realtime heart rate measurement in MI1S", ex); } @@ -675,6 +691,11 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } } + @Override + public void onSetConstantVibration(int intensity) { + + } + @Override public void onFetchActivityData() { try { @@ -695,6 +716,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { performInitialized(enable ? "Enabling realtime steps notifications" : "Disabling realtime steps notifications") .write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), enable ? getLowLatency() : getHighLatency()) .write(controlPoint, enable ? startRealTimeStepsNotifications : stopRealTimeStepsNotifications).queue(getQueue()); + enableRealtimeSamplesTimer(enable); } catch (IOException e) { LOG.error("Unable to change realtime steps notification to: " + enable, e); } @@ -767,105 +789,121 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { // not supported } + @Override + public void onAppReorder(UUID[] uuids) { + // not supported + } + @Override public void onScreenshotReq() { // not supported } @Override - public void onCharacteristicChanged(BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic) { + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { super.onCharacteristicChanged(gatt, characteristic); UUID characteristicUUID = characteristic.getUuid(); if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) { handleBatteryInfo(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS); + return true; } else if (MiBandService.UUID_CHARACTERISTIC_NOTIFICATION.equals(characteristicUUID)) { handleNotificationNotif(characteristic.getValue()); + return true; } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) { handleRealtimeSteps(characteristic.getValue()); - } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) { - handleRealtimeSteps(characteristic.getValue()); + return true; } else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { handleHeartrate(characteristic.getValue()); + return true; + } else if (MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA.equals(characteristicUUID)) { + handleSensorData(characteristic.getValue()); } else { LOG.info("Unhandled characteristic changed: " + characteristicUUID); logMessageContent(characteristic.getValue()); } + return false; } @Override - public void onCharacteristicRead(BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic, int status) { + public boolean onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicRead(gatt, characteristic, status); UUID characteristicUUID = characteristic.getUuid(); if (MiBandService.UUID_CHARACTERISTIC_DEVICE_INFO.equals(characteristicUUID)) { handleDeviceInfo(characteristic.getValue(), status); + return true; } else if (GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME.equals(characteristicUUID)) { handleDeviceName(characteristic.getValue(), status); + return true; } else if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) { handleBatteryInfo(characteristic.getValue(), status); + return true; } else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { - logHeartrate(characteristic.getValue()); + logHeartrate(characteristic.getValue(), status); + return true; } else if (MiBandService.UUID_CHARACTERISTIC_DATE_TIME.equals(characteristicUUID)) { - logDate(characteristic.getValue()); + logDate(characteristic.getValue(), status); + return true; } else { LOG.info("Unhandled characteristic read: " + characteristicUUID); logMessageContent(characteristic.getValue()); } + return false; } @Override - public void onCharacteristicWrite(BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic, int status) { + public boolean onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { UUID characteristicUUID = characteristic.getUuid(); if (MiBandService.UUID_CHARACTERISTIC_PAIR.equals(characteristicUUID)) { handlePairResult(characteristic.getValue(), status); + return true; } else if (MiBandService.UUID_CHARACTERISTIC_USER_INFO.equals(characteristicUUID)) { handleUserInfoResult(characteristic.getValue(), status); + return true; } else if (MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT.equals(characteristicUUID)) { handleControlPointResult(characteristic.getValue(), status); + return true; } + return false; } - /** - * Utility method that may be used to log incoming messages when we don't know how to deal with them yet. - * - * @param value - */ - public void logMessageContent(byte[] value) { - LOG.info("RECEIVED DATA WITH LENGTH: " + value.length); - for (byte b : value) { - LOG.warn("DATA: " + String.format("0x%2x", b)); - } - } - - public void logDate(byte[] value) { - GregorianCalendar calendar = MiBandDateConverter.rawBytesToCalendar(value); - LOG.info("Got Mi Band Date: " + DateTimeUtils.formatDateTime(calendar.getTime())); - } - - public void logHeartrate(byte[] value) { - LOG.info("Got heartrate:"); - if (value.length == 2 && value[0] == 6) { - int hrValue = (value[1] & 0xff); - GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO); + public void logDate(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + GregorianCalendar calendar = MiBandDateConverter.rawBytesToCalendar(value); + LOG.info("Got Mi Band Date: " + DateTimeUtils.formatDateTime(calendar.getTime())); } else { logMessageContent(value); } } + public void logHeartrate(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS && value != null) { + LOG.info("Got heartrate:"); + if (value.length == 2 && value[0] == 6) { + int hrValue = (value[1] & 0xff); + GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO); + } + return; + } + logMessageContent(value); + } + private void handleHeartrate(byte[] value) { if (value.length == 2 && value[0] == 6) { int hrValue = (value[1] & 0xff); if (LOG.isDebugEnabled()) { LOG.debug("heart rate: " + hrValue); } - Intent intent = new Intent(DeviceService.ACTION_HEARTRATE_MEASUREMENT) - .putExtra(DeviceService.EXTRA_HEART_RATE_VALUE, hrValue) - .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); - LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + RealtimeSamplesSupport realtimeSamplesSupport = getRealtimeSamplesSupport(); + realtimeSamplesSupport.setHeartrateBpm(hrValue); + if (!realtimeSamplesSupport.isRunning()) { + // single shot measurement, manually invoke storage and result publishing + realtimeSamplesSupport.triggerCurrentSample(); + } } } @@ -874,10 +912,75 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { if (LOG.isDebugEnabled()) { LOG.debug("realtime steps: " + steps); } - Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS) - .putExtra(DeviceService.EXTRA_REALTIME_STEPS, steps) - .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); - LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + getRealtimeSamplesSupport().setSteps(steps); + } + + private void enableRealtimeSamplesTimer(boolean enable) { + if (enable) { + getRealtimeSamplesSupport().start(); + } else { + if (realtimeSamplesSupport != null) { + realtimeSamplesSupport.stop(); + } + } + } + + public MiBandActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) { + MiBandActivitySample sample = new MiBandActivitySample(); + sample.setDevice(device); + sample.setUser(user); + sample.setTimestamp(timestampInSeconds); + sample.setProvider(provider); + + return sample; + } + + private RealtimeSamplesSupport getRealtimeSamplesSupport() { + if (realtimeSamplesSupport == null) { + realtimeSamplesSupport = new RealtimeSamplesSupport(1000, 1000) { + @Override + public void doCurrentSample() { + + try (DBHandler handler = GBApplication.acquireDB()) { + DaoSession session = handler.getDaoSession(); + + Device device = DBHelper.getDevice(getDevice(), session); + User user = DBHelper.getUser(session); + int ts = (int) (System.currentTimeMillis() / 1000); + MiBandSampleProvider provider = new MiBandSampleProvider(gbDevice, session); + MiBandActivitySample sample = createActivitySample(device, user, ts, provider); + sample.setHeartRate(getHeartrateBpm()); + sample.setSteps(getSteps()); + sample.setRawIntensity(ActivitySample.NOT_MEASURED); + sample.setRawKind(MiBandSampleProvider.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that? + + // TODO: remove this once fully ported to REALTIME_SAMPLES + if (sample.getSteps() != ActivitySample.NOT_MEASURED) { + Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS) + .putExtra(DeviceService.EXTRA_REALTIME_STEPS, sample.getSteps()) + .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + if (sample.getHeartRate() != ActivitySample.NOT_MEASURED) { + Intent intent = new Intent(DeviceService.ACTION_HEARTRATE_MEASUREMENT) + .putExtra(DeviceService.EXTRA_HEART_RATE_VALUE, sample.getHeartRate()) + .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + +// Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) +// .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample); +// LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + + LOG.debug("Storing realtime sample: " + sample); + provider.addGBActivitySample(sample); + } catch (Exception e) { + LOG.warn("Unable to acquire db for saving realtime samples", e); + } + } + }; + } + return realtimeSamplesSupport; } /** @@ -921,6 +1024,9 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { getDevice().sendDeviceUpdateIntent(getContext()); break; + case MiBandService.NOTIFY_SET_LATENCY_SUCCESS: + LOG.info("Setting latency succeeded."); + break; default: for (byte b : value) { LOG.warn("DATA: " + String.format("0x%2x", b)); @@ -942,9 +1048,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { mDeviceInfo = new DeviceInfo(value); mDeviceInfo.setTest1AHRMode(MI_1A_HR_FW_UPDATE_TEST_MODE_ENABLED); if (getDeviceInfo().supportsHeartrate()) { - getDevice().addDeviceInfo(new GenericItem( - getContext().getString(R.string.DEVINFO_HR_VER), - MiBandFWHelper.formatFirmwareVersion(mDeviceInfo.getHeartrateFirmwareVersion()))); + getDevice().setFirmwareVersion2(MiBandFWHelper.formatFirmwareVersion(mDeviceInfo.getHeartrateFirmwareVersion())); } LOG.warn("Device info: " + mDeviceInfo); versionCmd.hwVersion = mDeviceInfo.getHwVersion(); @@ -1074,22 +1178,8 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { int slotToUse = 2 - iteration; Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(mEvt.getBegin()); - byte[] calBytes = MiBandDateConverter.calendarToRawBytes(calendar); - - byte[] alarmMessage = new byte[]{ - MiBandService.COMMAND_SET_TIMER, - (byte) slotToUse, - (byte) 1, - calBytes[0], - calBytes[1], - calBytes[2], - calBytes[3], - calBytes[4], - calBytes[5], - (byte) 0, - (byte) 0 - }; - builder.write(characteristic, alarmMessage); + Alarm alarm = GBAlarm.createSingleShot(slotToUse, false, calendar); + queueAlarm(alarm, builder, characteristic); iteration++; } builder.queue(getQueue()); @@ -1099,5 +1189,45 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } } + @Override + public void onSendConfiguration(String config) { + // nothing yet + } + @Override + public void onTestNewFunction() { + try { + TransactionBuilder builder = performInitialized("Toggle sensor reading"); + if (isReadingSensorData) { + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), stopSensorRead); + isReadingSensorData = false; + } else { + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), startSensorRead); + isReadingSensorData = true; + } + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to toggle sensor reading MI", ex); + } + } + + private void handleSensorData(byte[] value) { + int counter=0, step=0, axis1=0, axis2=0, axis3 =0; + if ((value.length - 2) % 6 != 0) { + LOG.warn("GOT UNEXPECTED SENSOR DATA WITH LENGTH: " + value.length); + for (byte b : value) { + LOG.warn("DATA: " + String.format("0x%4x", b)); + } + } + else { + counter = (value[0] & 0xff) | ((value[1] & 0xff) << 8); + for (int idx = 0; idx < ((value.length - 2) / 6); idx++) { + step = idx * 6; + axis1 = (value[step+2] & 0xff) | ((value[step+3] & 0xff) << 8); + axis2 = (value[step+4] & 0xff) | ((value[step+5] & 0xff) << 8); + axis3 = (value[step+6] & 0xff) | ((value[step+7] & 0xff) << 8); + } + LOG.info("READ SENSOR DATA VALUES: counter:"+counter+" step:"+step+" axis1:"+axis1+" axis2:"+axis2+" axis3:"+axis3+";"); + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/RealtimeSamplesSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/RealtimeSamplesSupport.java new file mode 100644 index 00000000..a160af8c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/RealtimeSamplesSupport.java @@ -0,0 +1,86 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import java.util.Timer; +import java.util.TimerTask; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; + +/** + * Basic support for aggregating different sources of realtime data that comes in in a mostly + * fixed interval. The aggregated data will be stored together. + * + * start() and stop() may be called multiple times, but the first stop() call will really + * stop the timer. + * manner. + * + * Subclasses must implement #doCurrentSample() and should override #resetCurrentValues() + * (but call super!). + */ +public abstract class RealtimeSamplesSupport { + private final long delay; + private final long period; + + protected int steps; + protected int heartrateBpm; + // subclasses may add more + + private Timer realtimeStorageTimer; + + public RealtimeSamplesSupport(long delay, long period) { + this.delay = delay; + this.period = period; + } + + public synchronized void start() { + if (isRunning()) { + return; // already running + } + realtimeStorageTimer = new Timer("Mi Band Realtime Storage Timer"); + realtimeStorageTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + triggerCurrentSample(); + } + }, delay, period); + } + + public synchronized void stop() { + if (realtimeStorageTimer != null) { + realtimeStorageTimer.cancel(); + realtimeStorageTimer.purge(); + realtimeStorageTimer = null; + } + } + + public synchronized boolean isRunning() { + return realtimeStorageTimer != null; + } + + public void setSteps(int stepsPerMinute) { + this.steps = stepsPerMinute; + } + + public int getSteps() { + return steps; + } + + public void setHeartrateBpm(int hrBpm) { + this.heartrateBpm = hrBpm; + } + + public int getHeartrateBpm() { + return heartrateBpm; + } + + public void triggerCurrentSample() { + doCurrentSample(); + resetCurrentValues(); + } + + protected void resetCurrentValues() { + steps = ActivitySample.NOT_MEASURED; + heartrateBpm = ActivitySample.NOT_MEASURED; + } + + protected abstract void doCurrentSample(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java index 132ec182..ab28be7b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; @@ -16,9 +17,9 @@ public class V1NotificationStrategy implements NotificationStrategy { static final byte[] startVibrate = new byte[]{MiBandService.COMMAND_SEND_NOTIFICATION, 1}; static final byte[] stopVibrate = new byte[]{MiBandService.COMMAND_STOP_MOTOR_VIBRATE}; - private final MiBandSupport support; + private final AbstractBTLEDeviceSupport support; - public V1NotificationStrategy(MiBandSupport support) { + public V1NotificationStrategy(AbstractBTLEDeviceSupport support) { this.support = support; } @@ -61,6 +62,7 @@ public class V1NotificationStrategy implements NotificationStrategy { * @param extraAction an extra action to be executed after every vibration and flash sequence. Allows to abort the repetition, for example. * @param builder */ + @Override public void sendCustomNotification(VibrationProfile vibrationProfile, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) { BluetoothGattCharacteristic controlPoint = support.getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); for (short i = 0; i < vibrationProfile.getRepeat(); i++) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java index 32ed1275..8d213543 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java @@ -3,24 +3,29 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; import android.bluetooth.BluetoothGattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; public class V2NotificationStrategy implements NotificationStrategy { - private final MiBandSupport support; + private final AbstractBTLEDeviceSupport support; - public V2NotificationStrategy(MiBandSupport support) { + public V2NotificationStrategy(AbstractBTLEDeviceSupport support) { this.support = support; } + protected AbstractBTLEDeviceSupport getSupport() { + return support; + } + @Override public void sendDefaultNotification(TransactionBuilder builder, BtLEAction extraAction) { VibrationProfile profile = VibrationProfile.getProfile(VibrationProfile.ID_MEDIUM, (short) 3); sendCustomNotification(profile, extraAction, builder); } - private void sendCustomNotification(VibrationProfile vibrationProfile, BtLEAction extraAction, TransactionBuilder builder) { + protected void sendCustomNotification(VibrationProfile vibrationProfile, BtLEAction extraAction, TransactionBuilder builder) { //use the new alert characteristic BluetoothGattCharacteristic alert = support.getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL); for (short i = 0; i < vibrationProfile.getRepeat(); i++) { @@ -29,6 +34,8 @@ public class V2NotificationStrategy implements NotificationStrategy { int on = onOffSequence[j]; on = Math.min(500, on); // longer than 500ms is not possible builder.write(alert, new byte[]{GattCharacteristic.MILD_ALERT}); //MILD_ALERT lights up GREEN leds, HIGH_ALERT lights up RED leds +// builder.wait(on); +// builder.write(alert, new byte[]{GattCharacteristic.HIGH_ALERT}); builder.wait(on); builder.write(alert, new byte[]{GattCharacteristic.NO_ALERT}); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBand1Operation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBand1Operation.java new file mode 100644 index 00000000..00c1d7ba --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBand1Operation.java @@ -0,0 +1,17 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; + +public abstract class AbstractMiBand1Operation extends AbstractMiBandOperation { + protected AbstractMiBand1Operation(MiBandSupport support) { + super(support); + } + + @Override + protected void enableOtherNotifications(TransactionBuilder builder, boolean enable) { + builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable) + .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java index 2f6577d7..f0b143e3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java @@ -4,22 +4,23 @@ import android.widget.Toast; import java.io.IOException; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; -import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; -public abstract class AbstractMiBandOperation extends AbstractBTLEOperation { - protected AbstractMiBandOperation(MiBandSupport support) { +public abstract class AbstractMiBandOperation extends AbstractBTLEOperation { + protected AbstractMiBandOperation(T support) { super(support); } @Override protected void prePerform() throws IOException { super.prePerform(); + getDevice().setBusyTask("fetch activity data"); // mark as busy quickly to avoid interruptions from the outside TransactionBuilder builder = performInitialized("disabling some notifications"); enableOtherNotifications(builder, false); + enableNeededNotifications(builder, true); builder.queue(getQueue()); } @@ -27,9 +28,10 @@ public abstract class AbstractMiBandOperation extends AbstractBTLEOperation { + protected AbstractMiBand2Operation(MiBand2Support support) { + super(support); + } + + @Override + protected void enableOtherNotifications(TransactionBuilder builder, boolean enable) { + // TODO: check which notifications we should disable and re-enable here +// builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable) +// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/Mi2NotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/Mi2NotificationStrategy.java new file mode 100644 index 00000000..3cfed0a9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/Mi2NotificationStrategy.java @@ -0,0 +1,48 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2; + +import android.bluetooth.BluetoothGattCharacteristic; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.V2NotificationStrategy; + +public class Mi2NotificationStrategy extends V2NotificationStrategy { + + public Mi2NotificationStrategy(AbstractBTLEDeviceSupport support) { + super(support); + } + + @Override + protected void sendCustomNotification(VibrationProfile vibrationProfile, BtLEAction extraAction, TransactionBuilder builder) { + //use the new alert characteristic + BluetoothGattCharacteristic alert = getSupport().getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL); + for (short i = 0; i < vibrationProfile.getRepeat(); i++) { + int[] onOffSequence = vibrationProfile.getOnOffSequence(); + for (int j = 0; j < onOffSequence.length; j++) { + int on = onOffSequence[j]; + on = Math.min(500, on); // longer than 500ms is not possible + builder.write(alert, new byte[]{(byte) vibrationProfile.getAlertLevel()}); + builder.wait(on); + builder.write(alert, new byte[]{GattCharacteristic.NO_ALERT}); + + if (++j < onOffSequence.length) { + int off = Math.max(onOffSequence[j], 25); // wait at least 25ms + builder.wait(off); + } + + if (extraAction != null) { + builder.add(extraAction); + } + } + } + } + + @Override + public void sendCustomNotification(VibrationProfile vibrationProfile, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) { + // all other parameters are unfortunately not supported anymore ;-( + sendCustomNotification(vibrationProfile, extraAction, builder); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java new file mode 100644 index 00000000..881e5ca5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java @@ -0,0 +1,248 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.Logging; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.AbstractMiBand2Operation; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * An operation that fetches activity data. For every fetch, a new operation must + * be created, i.e. an operation may not be reused for multiple fetches. + */ +public class FetchActivityOperation extends AbstractMiBand2Operation { + private static final Logger LOG = LoggerFactory.getLogger(FetchActivityOperation.class); + + private List samples = new ArrayList<>(60*24); // 1day per default + + private byte lastPacketCounter = -1; + private Calendar startTimestamp; + + public FetchActivityOperation(MiBand2Support support) { + super(support); + } + + @Override + protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) { + if (!enable) { + // dynamically enabled, but always disabled on finish + builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable); + } + } + + @Override + protected void doPerform() throws IOException { + TransactionBuilder builder = performInitialized("fetching activity data"); + getSupport().setLowLatency(builder); + builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext())); + BluetoothGattCharacteristic characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4); + builder.notify(characteristicFetch, true); + BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA); + + GregorianCalendar sinceWhen = getLastSuccessfulSynchronizedTime(); + builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, 0x01 }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); + builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply + builder.notify(characteristicActivityData, true); + builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_ACTIVITY_DATA }); + builder.queue(getQueue()); + } + + private GregorianCalendar getLastSuccessfulSynchronizedTime() { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + DaoSession session = dbHandler.getDaoSession(); + SampleProvider sampleProvider = new MiBand2SampleProvider(getDevice(), session); + MiBandActivitySample sample = sampleProvider.getLatestActivitySample(); + if (sample != null) { + int timestamp = sample.getTimestamp(); + GregorianCalendar calendar = BLETypeConversions.createCalendar(); + calendar.setTimeInMillis((long) timestamp * 1000); + return calendar; + } + } catch (Exception ex) { + LOG.error("Error querying for latest activity sample, synchronizing the last 10 days", ex); + } + + GregorianCalendar calendar = BLETypeConversions.createCalendar(); + calendar.add(Calendar.DAY_OF_MONTH, -10); + return calendar; + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA.equals(characteristicUUID)) { + handleActivityNotif(characteristic.getValue()); + return true; + } else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) { + handleActivityMetadata(characteristic.getValue()); + return true; + } else { + return super.onCharacteristicChanged(gatt, characteristic); + } + } + + private void handleActivityFetchFinish() { + LOG.info("Fetching activity data has finished."); + saveSamples(); + operationFinished(); + unsetBusy(); + } + + private void saveSamples() { + if (samples.size() > 0) { + // save all the samples that we got + try (DBHandler handler = GBApplication.acquireDB()) { + DaoSession session = handler.getDaoSession(); + SampleProvider sampleProvider = new MiBandSampleProvider(getDevice(), session); + Device device = DBHelper.getDevice(getDevice(), session); + User user = DBHelper.getUser(session); + + GregorianCalendar timestamp = (GregorianCalendar) startTimestamp.clone(); + for (MiBandActivitySample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000)); + sample.setProvider(sampleProvider); + + if (LOG.isDebugEnabled()) { +// LOG.debug("sample: " + sample); + } + + timestamp.add(Calendar.MINUTE, 1); + } + sampleProvider.addGBActivitySamples(samples.toArray(new MiBandActivitySample[0])); + + LOG.info("Mi2 activity data: last sample timestamp: " + DateTimeUtils.formatDateTime(timestamp.getTime())); + + } catch (Exception ex) { + GB.toast(getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR); + } finally { + samples.clear(); + } + } + } + + /** + * Method to handle the incoming activity data. + * There are two kind of messages we currently know: + * - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.) + * - the second one is 20 bytes long and contains the actual activity data + *

+ * The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called. + * + * @param value + */ + private void handleActivityNotif(byte[] value) { + if (!isOperationRunning()) { + LOG.error("ignoring activity data notification because operation is not running. Data length: " + value.length); + getSupport().logMessageContent(value); + return; + } + + if ((value.length % 4) == 1) { + if ((byte) (lastPacketCounter + 1) == value[0] ) { + lastPacketCounter++; + bufferActivityData(value); + } else { + GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); + handleActivityFetchFinish(); + return; + } + } else { + GB.toast("Error fetching activity data, unexpected package length: " + value.length, Toast.LENGTH_LONG, GB.ERROR); + } + } + + /** + * Creates samples from the given 17-length array + * @param value + */ + private void bufferActivityData(byte[] value) { + int len = value.length; + + if (len % 4 != 1) { + throw new AssertionError("Unexpected activity array size: " + value); + } + + for (int i = 1; i < len; i+=4) { + MiBandActivitySample sample = createSample(value[i], value[i + 1], value[i + 2], value[i + 3]); + samples.add(sample); + } + } + + private MiBandActivitySample createSample(byte category, byte intensity, byte steps, byte heartrate) { + MiBandActivitySample sample = new MiBandActivitySample(); + sample.setRawKind(category & 0xff); + sample.setRawIntensity(intensity & 0xff); + sample.setSteps(steps & 0xff); + sample.setHeartRate(heartrate & 0xff); + + return sample; + } + + private void handleActivityMetadata(byte[] value) { + if (value.length == 15) { + // first two bytes are whether our request was accepted + if (ArrayUtils.equals(MiBand2Service.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, value, 0, 2)) { + // the third byte (0x01 on success) = ? + // the 4th - 7th bytes probably somehow represent the number of bytes/packets to expect + + // last 8 bytes are the start date + Calendar startTimestamp = getSupport().fromTimeBytes(org.apache.commons.lang3.ArrayUtils.subarray(value, 7, value.length)); + setStartTimestamp(startTimestamp); + + GB.toast(getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since, + DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), Toast.LENGTH_LONG, GB.INFO); + } else { + LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); + handleActivityFetchFinish(); + } + } else if (value.length == 3) { + if (Arrays.equals(MiBand2Service.RESPONSE_FINISH_SUCCESS, value)) { + handleActivityFetchFinish(); + } else { + LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); + handleActivityFetchFinish(); + } + } + } + + private void setStartTimestamp(Calendar startTimestamp) { + this.startTimestamp = startTimestamp; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/InitOperation.java new file mode 100644 index 00000000..5adabc67 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/InitOperation.java @@ -0,0 +1,139 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.UUID; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class InitOperation extends AbstractBTLEOperation { + private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class); + + private final TransactionBuilder builder; + private final boolean needsAuth; + + public InitOperation(boolean needsAuth, MiBand2Support support, TransactionBuilder builder) { + super(support); + this.needsAuth = needsAuth; + this.builder = builder; + builder.setGattCallback(this); + } + + @Override + protected void doPerform() throws IOException { + getSupport().enableNotifications(builder, true); + if (needsAuth) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); + // write key to miband2 + byte[] sendKey = org.apache.commons.lang3.ArrayUtils.addAll(new byte[]{MiBand2Service.AUTH_SEND_KEY, MiBand2Service.AUTH_BYTE}, getSecretKey()); + builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), sendKey); + } else { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + // get random auth number + builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), requestAuthNumber()); + } + } + + private byte[] requestAuthNumber() { + return new byte[]{MiBand2Service.AUTH_REQUEST_RANDOM_AUTH_NUMBER, MiBand2Service.AUTH_BYTE}; + } + + private byte[] getSecretKey() { + return new byte[]{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45}; + } + + @Override + public TransactionBuilder performInitialized(String taskName) throws IOException { + throw new UnsupportedOperationException("This IS the initialization class, you cannot call this method"); + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { + try { + byte[] value = characteristic.getValue(); + getSupport().logMessageContent(value); + if (value[0] == MiBand2Service.AUTH_RESPONSE && + value[1] == MiBand2Service.AUTH_SEND_KEY && + value[2] == MiBand2Service.AUTH_SUCCESS) { + TransactionBuilder builder = createTransactionBuilder("Sending the secret key to the band"); + builder.write(characteristic, requestAuthNumber()); + getSupport().performImmediately(builder); + } else if (value[0] == MiBand2Service.AUTH_RESPONSE && + value[1] == MiBand2Service.AUTH_REQUEST_RANDOM_AUTH_NUMBER && + value[2] == MiBand2Service.AUTH_SUCCESS) { + // md5?? + byte[] eValue = handleAESAuth(value, getSecretKey()); + byte[] responseValue = org.apache.commons.lang3.ArrayUtils.addAll( + new byte[]{MiBand2Service.AUTH_SEND_ENCRYPTED_AUTH_NUMBER, MiBand2Service.AUTH_BYTE}, eValue); + + TransactionBuilder builder = createTransactionBuilder("Sending the encrypted random key to the band"); + builder.write(characteristic, responseValue); + getSupport().setCurrentTimeWithService(builder); + getSupport().performImmediately(builder); + } else if (value[0] == MiBand2Service.AUTH_RESPONSE && + value[1] == MiBand2Service.AUTH_SEND_ENCRYPTED_AUTH_NUMBER && + value[2] == MiBand2Service.AUTH_SUCCESS) { + TransactionBuilder builder = createTransactionBuilder("Authenticated, now initialize phase 2"); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + getSupport().requestDeviceInfo(builder); + getSupport().phase2Initialize(builder); + getSupport().setInitialized(builder); + getSupport().performImmediately(builder); + } else { + return super.onCharacteristicChanged(gatt, characteristic); + } + } catch (Exception e) { + GB.toast(getContext(), "Error authenticating Mi Band 2", Toast.LENGTH_LONG, GB.ERROR, e); + } + return true; + } else { + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + return super.onCharacteristicChanged(gatt, characteristic); + } + } + + private TransactionBuilder createTransactionBuilder(String task) { + TransactionBuilder builder = getSupport().createTransactionBuilder(task); + builder.setGattCallback(this); + return builder; + } + + private byte[] getMD5(byte[] message) throws NoSuchAlgorithmException { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return md5.digest(message); + } + + private byte[] handleAESAuth(byte[] value, byte[] secretKey) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException { + byte[] mValue = Arrays.copyOfRange(value, 3, 19); + Cipher ecipher = Cipher.getInstance("AES/ECB/NoPadding"); + SecretKeySpec newKey = new SecretKeySpec(secretKey, "AES"); + ecipher.init(Cipher.ENCRYPT_MODE, newKey); + byte[] enc = ecipher.doFinal(mValue); + return enc; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandler.java index 710178f9..1944f2d5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandler.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; public class AppMessageHandler { protected final PebbleProtocol mPebbleProtocol; @@ -32,4 +33,8 @@ public class AppMessageHandler { public GBDeviceEvent[] pushMessage() { return null; } + + protected GBDevice getDevice() { + return mPebbleProtocol.getDevice(); + } } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerGBPebble.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerGBPebble.java deleted file mode 100644 index f3ac052f..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerGBPebble.java +++ /dev/null @@ -1,78 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; - -import android.util.Pair; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.SimpleTimeZone; -import java.util.TimeZone; -import java.util.UUID; - -import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.GBException; -import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; -import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; -import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; -import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; - -public class AppMessageHandlerGBPebble extends AppMessageHandler { - - public static final int KEY_TIMESTAMP = 1; - public static final int KEY_SAMPLES = 2; - - private static final Logger LOG = LoggerFactory.getLogger(AppMessageHandlerGBPebble.class); - - AppMessageHandlerGBPebble(UUID uuid, PebbleProtocol pebbleProtocol) { - super(uuid, pebbleProtocol); - } - - @Override - public GBDeviceEvent[] handleMessage(ArrayList> pairs) { - int timestamp = 0; - for (Pair pair : pairs) { - switch (pair.first) { - case KEY_TIMESTAMP: - TimeZone tz = SimpleTimeZone.getDefault(); - timestamp = (int) pair.second - (tz.getOffset(System.currentTimeMillis())) / 1000; - LOG.info("got timestamp " + timestamp); - break; - case KEY_SAMPLES: - byte[] samples = (byte[]) pair.second; - ByteBuffer samplesBuffer = ByteBuffer.wrap(samples); - samplesBuffer.order(ByteOrder.LITTLE_ENDIAN); - int samples_remaining = samples.length / 2; - LOG.info("got " + samples_remaining + " samples"); - int offset_seconds = 0; - DBHandler db = null; - try { - db = GBApplication.acquireDB(); - while (samples_remaining-- > 0) { - short sample = samplesBuffer.getShort(); - int type = ((sample & 0xe000) >>> 13); - int intensity = ((sample & 0x1f80) >>> 7); - int steps = (sample & 0x007f); - db.addGBActivitySample(timestamp + offset_seconds, SampleProvider.PROVIDER_PEBBLE_GADGETBRIDGE, intensity, steps, type, 0); - offset_seconds += 60; - } - } catch (GBException e) { - LOG.error("Error acquiring database", e); - } finally { - if (db != null) { - db.release(); - } - } - break; - default: - LOG.info("unhandled key: " + pair.first); - break; - } - } - GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); - sendBytes.encodedBytes = mPebbleProtocol.encodeApplicationMessageAck(mUUID, mPebbleProtocol.last_id); - return new GBDeviceEvent[]{sendBytes}; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMisfit.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMisfit.java index 1a7e4b97..1bb51991 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMisfit.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMisfit.java @@ -13,13 +13,13 @@ import java.util.SimpleTimeZone; import java.util.UUID; 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.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; -import nodomain.freeyourgadget.gadgetbridge.devices.pebble.MisfitSampleProvider; -import nodomain.freeyourgadget.gadgetbridge.impl.GBActivitySample; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleMisfitSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMisfitSample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class AppMessageHandlerMisfit extends AppMessageHandler { @@ -40,8 +40,6 @@ public class AppMessageHandlerMisfit extends AppMessageHandler { super(uuid, pebbleProtocol); } - private final MisfitSampleProvider sampleProvider = new MisfitSampleProvider(); - @Override public boolean isEnabled() { Prefs prefs = GBApplication.getPrefs(); @@ -50,6 +48,7 @@ public class AppMessageHandlerMisfit extends AppMessageHandler { @Override public GBDeviceEvent[] handleMessage(ArrayList> pairs) { + GBDevice device = getDevice(); for (Pair pair : pairs) { switch (pair.first) { case KEY_INCOMING_DATA_BEGIN: @@ -69,7 +68,7 @@ public class AppMessageHandlerMisfit extends AppMessageHandler { break; } - if (!mPebbleProtocol.isFw3x) { + if (mPebbleProtocol.mFwMajor < 3) { timestamp -= SimpleTimeZone.getDefault().getOffset(timestamp * 1000L) / 1000; } Date startDate = new Date((long) timestamp * 1000L); @@ -77,52 +76,26 @@ public class AppMessageHandlerMisfit extends AppMessageHandler { LOG.info("got data from " + startDate + " to " + endDate); int totalSteps = 0; - GBActivitySample[] activitySamples = new GBActivitySample[samples]; - for (int i = 0; i < samples; i++) { - short sample = buf.getShort(); - int steps = 0; - int intensity = 0; - int activityKind = ActivityKind.TYPE_UNKNOWN; + PebbleMisfitSample[] misfitSamples = new PebbleMisfitSample[samples]; + try (DBHandler db = GBApplication.acquireDB()) { + PebbleMisfitSampleProvider sampleProvider = new PebbleMisfitSampleProvider(device, db.getDaoSession()); + Long userId = DBHelper.getUser(db.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId(); + for (int i = 0; i < samples; i++) { + short sample = buf.getShort(); + misfitSamples[i] = new PebbleMisfitSample(timestamp + i * 60, deviceId, userId, sample & 0xffff); + misfitSamples[i].setProvider(sampleProvider); + int steps = misfitSamples[i].getSteps(); + totalSteps += steps; + LOG.info("got steps for sample " + i + " : " + steps + "(" + Integer.toHexString(sample & 0xffff) + ")"); - 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; } + LOG.info("total steps for above period: " + totalSteps); - totalSteps += steps; - LOG.info("got steps for sample " + i + " : " + steps + "(" + Integer.toHexString(sample & 0xffff) + ")"); - - activitySamples[i] = new GBActivitySample(sampleProvider, timestamp + i * 60, intensity, steps, activityKind); - } - LOG.info("total steps for above period: " + totalSteps); - - DBHandler db = null; - try { - db = GBApplication.acquireDB(); - db.addGBActivitySamples(activitySamples); - } catch (GBException e) { + sampleProvider.addGBActivitySamples(misfitSamples); + } catch (Exception e) { LOG.error("Error acquiring database", e); return null; - } finally { - if (db != null) { - db.release(); - } } break; default: diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMorpheuz.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMorpheuz.java index 62f4d39a..4f7a14a8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMorpheuz.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerMorpheuz.java @@ -11,34 +11,49 @@ import java.util.TimeZone; import java.util.UUID; 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.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepMonitorResult; -import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.devices.pebble.MorpheuzSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleMorpheuzSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSample; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class AppMessageHandlerMorpheuz extends AppMessageHandler { public static final int KEY_POINT = 1; + public static final int KEY_POINT_46 = 10000; public static final int KEY_CTRL = 2; + public static final int KEY_CTRL_46 = 10001; public static final int KEY_FROM = 3; + public static final int KEY_FROM_46 = 10002; public static final int KEY_TO = 4; + public static final int KEY_TO_46 = 10003; public static final int KEY_BASE = 5; + public static final int KEY_BASE_46 = 10004; public static final int KEY_VERSION = 6; + public static final int KEY_VERSION_46 = 10005; public static final int KEY_GONEOFF = 7; + public static final int KEY_GONEOFF_46 = 10006; public static final int KEY_TRANSMIT = 8; + public static final int KEY_TRANSMIT_46 = 10007; public static final int KEY_AUTO_RESET = 9; + public static final int KEY_AUTO_RESET_46 = 10008; + public static final int KEY_SNOOZES = 10; + public static final int KEY_SNOOZES_46 = 10009; + public static final int KEY_FAULT_46 = 10010; public static final int CTRL_TRANSMIT_DONE = 1; public static final int CTRL_VERSION_DONE = 2; public static final int CTRL_GONEOFF_DONE = 4; public static final int CTRL_DO_NEXT = 8; public static final int CTRL_SET_LAST_SENT = 16; + public static final int CTRL_LAZARUS = 32; + public static final int CTRL_SNOOZES_DONE = 64; // data received from Morpheuz in native format + private int version = 0; private int smartalarm_from = -1; // time in minutes relative from 0:00 for smart alarm (earliest) private int smartalarm_to = -1;// time in minutes relative from 0:00 for smart alarm (latest) private int recording_base_timestamp = -1; // timestamp for the first "point", all folowing are +10 minutes offset each @@ -71,71 +86,82 @@ public class AppMessageHandlerMorpheuz extends AppMessageHandler { for (Pair pair : pairs) { switch (pair.first) { case KEY_TRANSMIT: - case KEY_GONEOFF: - if (pair.first == KEY_GONEOFF) { - alarm_gone_off = (int) pair.second; - LOG.info("got gone off: " + alarm_gone_off / 60 + ":" + alarm_gone_off % 60); - } + case KEY_TRANSMIT_46: sleepMonitorResult = new GBDeviceEventSleepMonitorResult(); sleepMonitorResult.smartalarm_from = smartalarm_from; sleepMonitorResult.smartalarm_to = smartalarm_to; sleepMonitorResult.alarm_gone_off = alarm_gone_off; sleepMonitorResult.recording_base_timestamp = recording_base_timestamp; + ctrl_message |= CTRL_TRANSMIT_DONE; + break; + case KEY_GONEOFF: + case KEY_GONEOFF_46: + alarm_gone_off = (int) pair.second; + LOG.info("got gone off: " + alarm_gone_off / 60 + ":" + alarm_gone_off % 60); + ctrl_message |= CTRL_DO_NEXT | CTRL_GONEOFF_DONE; break; case KEY_POINT: + case KEY_POINT_46: if (recording_base_timestamp == -1) { // we have no base timestamp but received points, stop this - ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE | AppMessageHandlerMorpheuz.CTRL_GONEOFF_DONE | AppMessageHandlerMorpheuz.CTRL_TRANSMIT_DONE | AppMessageHandlerMorpheuz.CTRL_SET_LAST_SENT; + ctrl_message = CTRL_VERSION_DONE | CTRL_GONEOFF_DONE | CTRL_TRANSMIT_DONE | CTRL_SET_LAST_SENT; } else { int index = ((int) pair.second >> 16); int intensity = ((int) pair.second & 0xffff); LOG.info("got point:" + index + " " + intensity); - int type = MorpheuzSampleProvider.TYPE_UNKNOWN; - if (intensity <= 120) { - type = MorpheuzSampleProvider.TYPE_DEEP_SLEEP; - } else if (intensity <= 1000) { - type = MorpheuzSampleProvider.TYPE_LIGHT_SLEEP; - } if (index >= 0) { - DBHandler db = null; - try { - db = GBApplication.acquireDB(); - db.addGBActivitySample(recording_base_timestamp + index * 600, SampleProvider.PROVIDER_PEBBLE_MORPHEUZ, intensity, 0, type, 0); - } catch (GBException e) { + try (DBHandler db = GBApplication.acquireDB()) { + Long userId = DBHelper.getUser(db.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId(); + PebbleMorpheuzSampleProvider sampleProvider = new PebbleMorpheuzSampleProvider(getDevice(), db.getDaoSession()); + PebbleMorpheuzSample sample = new PebbleMorpheuzSample(recording_base_timestamp + index * 600, deviceId, userId, intensity); + sample.setProvider(sampleProvider); + sampleProvider.addGBActivitySample(sample); + } catch (Exception e) { LOG.error("Error acquiring database", e); - } finally { - if (db != null) { - db.release(); - } } } - ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE | AppMessageHandlerMorpheuz.CTRL_SET_LAST_SENT | AppMessageHandlerMorpheuz.CTRL_DO_NEXT; + ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT; } break; case KEY_FROM: + case KEY_FROM_46: smartalarm_from = (int) pair.second; LOG.info("got from: " + smartalarm_from / 60 + ":" + smartalarm_from % 60); - ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE | AppMessageHandlerMorpheuz.CTRL_SET_LAST_SENT | AppMessageHandlerMorpheuz.CTRL_DO_NEXT; + ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT; break; case KEY_TO: + case KEY_TO_46: smartalarm_to = (int) pair.second; LOG.info("got to: " + smartalarm_to / 60 + ":" + smartalarm_to % 60); - ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE | AppMessageHandlerMorpheuz.CTRL_SET_LAST_SENT | AppMessageHandlerMorpheuz.CTRL_DO_NEXT; + ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT; break; case KEY_VERSION: - LOG.info("got version: " + ((float) ((int) pair.second) / 10.0f)); - ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE; + case KEY_VERSION_46: + version = (int) pair.second; + LOG.info("got version: " + ((float) version / 10.0f)); + ctrl_message |= CTRL_VERSION_DONE; break; case KEY_BASE: + case KEY_BASE_46: // fix timestamp TimeZone tz = SimpleTimeZone.getDefault(); recording_base_timestamp = (int) pair.second - (tz.getOffset(System.currentTimeMillis())) / 1000; LOG.info("got base: " + recording_base_timestamp); - ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE | AppMessageHandlerMorpheuz.CTRL_SET_LAST_SENT | AppMessageHandlerMorpheuz.CTRL_DO_NEXT; + ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT; break; case KEY_AUTO_RESET: - ctrl_message = AppMessageHandlerMorpheuz.CTRL_VERSION_DONE | AppMessageHandlerMorpheuz.CTRL_SET_LAST_SENT | AppMessageHandlerMorpheuz.CTRL_DO_NEXT; + case KEY_AUTO_RESET_46: + ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT; + break; + case KEY_SNOOZES: + case KEY_SNOOZES_46: + ctrl_message |= CTRL_SNOOZES_DONE | CTRL_DO_NEXT; + break; + case KEY_FAULT_46: + LOG.info("fault code: " + (int) pair.second); + ctrl_message |= CTRL_DO_NEXT; break; default: LOG.info("unhandled key: " + pair.first); @@ -151,7 +177,11 @@ public class AppMessageHandlerMorpheuz extends AppMessageHandler { GBDeviceEventSendBytes sendBytesCtrl = null; if (ctrl_message > 0) { sendBytesCtrl = new GBDeviceEventSendBytes(); - sendBytesCtrl.encodedBytes = encodeMorpheuzMessage(AppMessageHandlerMorpheuz.KEY_CTRL, ctrl_message); + int ctrlkey = KEY_CTRL; + if (version >= 46) { + ctrlkey = KEY_CTRL_46; + } + sendBytesCtrl.encodedBytes = encodeMorpheuzMessage(ctrlkey, ctrl_message); } // ctrl and sleep monitor might be null, thats okay diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java index e25a28ea..20e6165f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/AppMessageHandlerPebStyle.java @@ -58,7 +58,7 @@ public class AppMessageHandlerPebStyle extends AppMessageHandler { pairs.add(new Pair<>(KEY_SECOND_HAND, (Object) 0)); //1 enabled pairs.add(new Pair<>(KEY_BLUETOOTH_ALERT, (Object) 0)); //1 silent, 2 weak, up to 5 pairs.add(new Pair<>(KEY_TEMPERATURE_FORMAT, (Object) 1)); //0 fahrenheit - //pairs.add(new Pair<>(KEY_LOCATION_SERVICE, (Object) 2)); //0 uto, 1 manual + pairs.add(new Pair<>(KEY_LOCATION_SERVICE, (Object) 2)); //0 auto, 1 manual pairs.add(new Pair<>(KEY_SIDEBAR_LOCATION, (Object) 1)); //0 right pairs.add(new Pair<>(KEY_COLOR_SELECTION, (Object) 1)); //1 custom pairs.add(new Pair<>(KEY_MAIN_COLOR, (Object) PebbleColor.Black)); @@ -118,4 +118,4 @@ public class AppMessageHandlerPebStyle extends AppMessageHandler { return new GBDeviceEvent[]{sendBytes}; */ } -} \ No newline at end of file +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthHR.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthHR.java new file mode 100644 index 00000000..2f70e1f4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthHR.java @@ -0,0 +1,27 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +class DatalogSessionHealthHR extends DatalogSessionPebbleHealth { + + private static final Logger LOG = LoggerFactory.getLogger(DatalogSessionHealthHR.class); + + DatalogSessionHealthHR(byte id, UUID uuid, int tag, byte item_type, short item_size, GBDevice device) { + super(id, uuid, tag, item_type, item_size, device); + taginfo = "(Health - HR " + tag + " )"; + } + + @Override + public boolean handleMessage(ByteBuffer datalogMessage, int length) { + LOG.info("DATALOG " + taginfo + GB.hexdump(datalogMessage.array(), datalogMessage.position(), length)); + + return isPebbleHealthEnabled(); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthOverlayData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthOverlayData.java index 35d84be2..e41d8f32 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthOverlayData.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthOverlayData.java @@ -4,22 +4,27 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; -import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.devices.pebble.HealthSampleProvider; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlay; +import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlayDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.GB; class DatalogSessionHealthOverlayData extends DatalogSessionPebbleHealth { private static final Logger LOG = LoggerFactory.getLogger(DatalogSessionHealthOverlayData.class); - public DatalogSessionHealthOverlayData(byte id, UUID uuid, int tag, byte item_type, short item_size) { - super(id, uuid, tag, item_type, item_size); - taginfo = "(health - overlay data " + tag + " )"; + public DatalogSessionHealthOverlayData(byte id, UUID uuid, int tag, byte item_type, short item_size, GBDevice device) { + super(id, uuid, tag, item_type, item_size, device); + taginfo = "(Health - overlay data " + tag + " )"; } @Override @@ -40,64 +45,65 @@ class DatalogSessionHealthOverlayData extends DatalogSessionPebbleHealth { int recordCount = length / itemSize; OverlayRecord[] overlayRecords = new OverlayRecord[recordCount]; + byte[] tempRecord = new byte[itemSize]; for (int recordIdx = 0; recordIdx < recordCount; recordIdx++) { beginOfRecordPosition = initialPosition + recordIdx * itemSize; datalogMessage.position(beginOfRecordPosition);//we may not consume all the bytes of a record - recordVersion = datalogMessage.getShort(); - if ((recordVersion != 1) && (recordVersion != 3)) - return false;//we don't know how to deal with the data TODO: this is not ideal because we will get the same message again and again since we NACK it - - datalogMessage.getShort();//throwaway, unknown - recordType = datalogMessage.getShort(); - - overlayRecords[recordIdx] = new OverlayRecord(recordType, datalogMessage.getInt(), datalogMessage.getInt(), datalogMessage.getInt()); + datalogMessage.get(tempRecord); + overlayRecords[recordIdx] = new OverlayRecord(tempRecord); } - return store(overlayRecords);//NACK if we cannot store the data yet, the watch will send the overlay records again. - } - - private boolean store(OverlayRecord[] overlayRecords) { - DBHandler dbHandler = null; - SampleProvider sampleProvider = new HealthSampleProvider(); - try { - dbHandler = GBApplication.acquireDB(); - int latestTimestamp = dbHandler.fetchLatestTimestamp(sampleProvider); - for (OverlayRecord overlayRecord : overlayRecords) { - if (latestTimestamp < (overlayRecord.timestampStart + overlayRecord.durationSeconds)) - return false; - switch (overlayRecord.type) { - case 1: - dbHandler.changeStoredSamplesType(overlayRecord.timestampStart, (overlayRecord.timestampStart + overlayRecord.durationSeconds), sampleProvider.toRawActivityKind(ActivityKind.TYPE_ACTIVITY), sampleProvider.toRawActivityKind(ActivityKind.TYPE_LIGHT_SLEEP), sampleProvider); - break; - case 2: - dbHandler.changeStoredSamplesType(overlayRecord.timestampStart, (overlayRecord.timestampStart + overlayRecord.durationSeconds), sampleProvider.toRawActivityKind(ActivityKind.TYPE_DEEP_SLEEP), sampleProvider); - break; - default: - //TODO: other values refer to unknown activity types. - } - } - } catch (Exception ex) { - LOG.debug(ex.getMessage()); - } finally { - if (dbHandler != null) { - dbHandler.release(); - } - } + store(overlayRecords); return true; } + private void store(OverlayRecord[] overlayRecords) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + DaoSession session = dbHandler.getDaoSession(); + Long userId = DBHelper.getUser(session).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), session).getId(); + + PebbleHealthActivityOverlayDao overlayDao = session.getPebbleHealthActivityOverlayDao(); + + List overlayList = new ArrayList<>(); + for (OverlayRecord overlayRecord : overlayRecords) { + overlayList.add(new PebbleHealthActivityOverlay(overlayRecord.timestampStart, overlayRecord.timestampStart + overlayRecord.durationSeconds, overlayRecord.type, deviceId, userId, overlayRecord.getRawData())); + } + overlayDao.insertOrReplaceInTx(overlayList); + } catch (Exception ex) { + LOG.debug(ex.getMessage()); + } + } + private class OverlayRecord { + byte[] knownVersions = {1, 3}; + short version; int type; //1=sleep, 2=deep sleep int offsetUTC; //probably int timestampStart; int durationSeconds; + byte[] rawData; - public OverlayRecord(int type, int offsetUTC, int timestampStart, int durationSeconds) { - this.type = type; - this.offsetUTC = offsetUTC; - this.timestampStart = timestampStart; - this.durationSeconds = durationSeconds; + OverlayRecord(byte[] rawData) { + this.rawData = rawData; + ByteBuffer record = ByteBuffer.wrap(rawData); + record.order(ByteOrder.LITTLE_ENDIAN); + + this.version = record.getShort(); + //TODO: check supported versions? + record.getShort();//throwaway, unknown + this.type = record.getShort(); + this.offsetUTC = record.getInt(); + this.timestampStart = record.getInt(); + this.durationSeconds = record.getInt(); + } + + byte[] getRawData() { + if (storePebbleHealthRawRecord()) { + return rawData; + } + return null; } } } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java index 455e6459..d6534ef7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSleep.java @@ -4,22 +4,27 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; -import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.devices.pebble.HealthSampleProvider; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlay; +import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivityOverlayDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.GB; class DatalogSessionHealthSleep extends DatalogSessionPebbleHealth { private static final Logger LOG = LoggerFactory.getLogger(DatalogSessionHealthSleep.class); - public DatalogSessionHealthSleep(byte id, UUID uuid, int tag, byte item_type, short item_size) { - super(id, uuid, tag, item_type, item_size); - taginfo = "(health - sleep " + tag + " )"; + public DatalogSessionHealthSleep(byte id, UUID uuid, int tag, byte item_type, short item_size, GBDevice device) { + super(id, uuid, tag, item_type, item_size, device); + taginfo = "(Health - sleep " + tag + " )"; } @Override @@ -39,55 +44,68 @@ class DatalogSessionHealthSleep extends DatalogSessionPebbleHealth { int recordCount = length / itemSize; SleepRecord[] sleepRecords = new SleepRecord[recordCount]; + byte[] tempRecord = new byte[itemSize]; for (int recordIdx = 0; recordIdx < recordCount; recordIdx++) { beginOfRecordPosition = initialPosition + recordIdx * itemSize; datalogMessage.position(beginOfRecordPosition);//we may not consume all the bytes of a record - recordVersion = datalogMessage.getShort(); - if (recordVersion != 1) - return false;//we don't know how to deal with the data TODO: this is not ideal because we will get the same message again and again since we NACK it + datalogMessage.get(tempRecord); - sleepRecords[recordIdx] = new SleepRecord(datalogMessage.getInt(), - datalogMessage.getInt(), - datalogMessage.getInt(), - datalogMessage.getInt()); + sleepRecords[recordIdx] = new SleepRecord(tempRecord); } - return store(sleepRecords);//NACK if we cannot store the data yet, the watch will send the sleep records again. - } - - private boolean store(SleepRecord[] sleepRecords) { - DBHandler dbHandler = null; - SampleProvider sampleProvider = new HealthSampleProvider(); - try { - dbHandler = GBApplication.acquireDB(); - int latestTimestamp = dbHandler.fetchLatestTimestamp(sampleProvider); - for (SleepRecord sleepRecord : sleepRecords) { - if (latestTimestamp < sleepRecord.bedTimeEnd) - return false; - dbHandler.changeStoredSamplesType(sleepRecord.bedTimeStart, sleepRecord.bedTimeEnd, sampleProvider.toRawActivityKind(ActivityKind.TYPE_ACTIVITY), sampleProvider.toRawActivityKind(ActivityKind.TYPE_LIGHT_SLEEP), sampleProvider); - } - } catch (Exception ex) { - LOG.debug(ex.getMessage()); - } finally { - if (dbHandler != null) { - dbHandler.release(); - } - } + store(sleepRecords); return true; } + private void store(SleepRecord[] sleepRecords) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + DaoSession session = dbHandler.getDaoSession(); + Long userId = DBHelper.getUser(session).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), session).getId(); + + PebbleHealthActivityOverlayDao overlayDao = session.getPebbleHealthActivityOverlayDao(); + + List overlayList = new ArrayList<>(); + for (SleepRecord sleepRecord : sleepRecords) { + //TODO: check the firmware version and don't use the sleep record if overlay is available? + overlayList.add(new PebbleHealthActivityOverlay(sleepRecord.bedTimeStart, sleepRecord.bedTimeEnd, sleepRecord.type, deviceId, userId, sleepRecord.getRawData())); + } + overlayDao.insertOrReplaceInTx(overlayList); + } catch (Exception ex) { + LOG.debug(ex.getMessage()); + } + } + private class SleepRecord { + byte[] knownVersions = {1}; + short version; + int type = 1; //sleep, hardcoded as we don't get other info int offsetUTC; //probably int bedTimeStart; int bedTimeEnd; int deepSleepSeconds; + byte[] rawData; - public SleepRecord(int offsetUTC, int bedTimeStart, int bedTimeEnd, int deepSleepSeconds) { - this.offsetUTC = offsetUTC; - this.bedTimeStart = bedTimeStart; - this.bedTimeEnd = bedTimeEnd; - this.deepSleepSeconds = deepSleepSeconds; + SleepRecord(byte[] rawData) { + this.rawData = rawData; + ByteBuffer record = ByteBuffer.wrap(rawData); + record.order(ByteOrder.LITTLE_ENDIAN); + + + this.version = record.getShort(); + //TODO: check supported versions? + this.offsetUTC = record.getInt(); + this.bedTimeStart = record.getInt(); + this.bedTimeEnd = record.getInt(); + this.deepSleepSeconds = record.getInt(); + } + + byte[] getRawData() { + if (storePebbleHealthRawRecord()) { + return rawData; + } + return null; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java index 06151d7a..4bcc6ae5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionHealthSteps.java @@ -5,24 +5,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; -import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.devices.pebble.HealthSampleProvider; -import nodomain.freeyourgadget.gadgetbridge.impl.GBActivitySample; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; -import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleHealthSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class DatalogSessionHealthSteps extends DatalogSessionPebbleHealth { private static final Logger LOG = LoggerFactory.getLogger(DatalogSessionHealthSteps.class); - public DatalogSessionHealthSteps(byte id, UUID uuid, int tag, byte item_type, short item_size) { - super(id, uuid, tag, item_type, item_size); - taginfo = "(health - steps)"; + public DatalogSessionHealthSteps(byte id, UUID uuid, int tag, byte item_type, short item_size, GBDevice device) { + super(id, uuid, tag, item_type, item_size, device); + taginfo = "(Health - steps)"; } @Override @@ -50,7 +50,7 @@ public class DatalogSessionHealthSteps extends DatalogSessionPebbleHealth { recordVersion = datalogMessage.getShort(); - if ((recordVersion != 5) && (recordVersion != 6)) + if ((recordVersion != 5) && (recordVersion != 6) && (recordVersion != 7) && (recordVersion != 12) && (recordVersion != 13)) return false; //we don't know how to deal with the data TODO: this is not ideal because we will get the same message again and again since we NACK it timestamp = datalogMessage.getInt(); @@ -60,10 +60,12 @@ public class DatalogSessionHealthSteps extends DatalogSessionPebbleHealth { beginOfRecordPosition = datalogMessage.position(); StepsRecord[] stepsRecords = new StepsRecord[recordNum]; + byte[] tempRecord = new byte[recordLength]; for (int recordIdx = 0; recordIdx < recordNum; recordIdx++) { datalogMessage.position(beginOfRecordPosition + recordIdx * recordLength); //we may not consume all the bytes of a record - stepsRecords[recordIdx] = new StepsRecord(timestamp, datalogMessage.get() & 0xff, datalogMessage.get() & 0xff, datalogMessage.getShort() & 0xffff, datalogMessage.get() & 0xff); + datalogMessage.get(tempRecord); + stepsRecords[recordIdx] = new StepsRecord(timestamp, recordVersion, tempRecord); timestamp += 60; } @@ -74,45 +76,70 @@ public class DatalogSessionHealthSteps extends DatalogSessionPebbleHealth { private void store(StepsRecord[] stepsRecords) { - DBHandler dbHandler = null; - SampleProvider sampleProvider = new HealthSampleProvider(); + try (DBHandler dbHandler = GBApplication.acquireDB()) { + PebbleHealthSampleProvider sampleProvider = new PebbleHealthSampleProvider(getDevice(), dbHandler.getDaoSession()); + PebbleHealthActivitySample[] samples = new PebbleHealthActivitySample[stepsRecords.length]; + // TODO: user and device + Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); + for (int j = 0; j < stepsRecords.length; j++) { + StepsRecord stepsRecord = stepsRecords[j]; + samples[j] = new PebbleHealthActivitySample( + stepsRecord.timestamp, + deviceId, userId, + stepsRecord.getRawData(), + stepsRecord.intensity, + stepsRecord.steps, + stepsRecord.heart_rate + ); + samples[j].setProvider(sampleProvider); + } - ActivitySample[] samples = new ActivitySample[stepsRecords.length]; - for (int j = 0; j < stepsRecords.length; j++) { - StepsRecord stepsRecord = stepsRecords[j]; - samples[j] = new GBActivitySample( - sampleProvider, - stepsRecord.timestamp, - stepsRecord.intensity, - stepsRecord.steps, - sampleProvider.toRawActivityKind(ActivityKind.TYPE_ACTIVITY)); - } - - try { - dbHandler = GBApplication.acquireDB(); - dbHandler.addGBActivitySamples(samples); + sampleProvider.addGBActivitySamples(samples); } catch (Exception ex) { LOG.debug(ex.getMessage()); - } finally { - if (dbHandler != null) { - dbHandler.release(); - } } } private class StepsRecord { + byte[] knownVersions = {5, 6, 7, 12, 13}; + short version; int timestamp; int steps; int orientation; int intensity; int light_intensity; + int heart_rate; - public StepsRecord(int timestamp, int steps, int orientation, int intensity, int light_intensity) { + byte[] rawData; + + StepsRecord(int timestamp, short version, byte[] rawData) { this.timestamp = timestamp; - this.steps = steps; - this.orientation = orientation; - this.intensity = intensity; - this.light_intensity = light_intensity; + this.rawData = rawData; + ByteBuffer record = ByteBuffer.wrap(rawData); + record.order(ByteOrder.LITTLE_ENDIAN); + + this.version = version; + //TODO: check supported versions? + + this.steps = record.get() & 0xff; + this.orientation = record.get() & 0xff; + this.intensity = record.getShort() & 0xffff; + this.light_intensity = record.get() & 0xff; + if (version >= 7) { + // skip 7 bytes + record.getInt(); + record.getShort(); + record.get(); + this.heart_rate = record.get() & 0xff; + } + } + + byte[] getRawData() { + if (storePebbleHealthRawRecord()) { + return rawData; + } + return null; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionPebbleHealth.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionPebbleHealth.java index 6df7a751..2aae2cb9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionPebbleHealth.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/DatalogSessionPebbleHealth.java @@ -3,16 +3,29 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; abstract class DatalogSessionPebbleHealth extends DatalogSession { - DatalogSessionPebbleHealth(byte id, UUID uuid, int tag, byte itemType, short itemSize) { + private final GBDevice mDevice; + + DatalogSessionPebbleHealth(byte id, UUID uuid, int tag, byte itemType, short itemSize, GBDevice device) { super(id, uuid, tag, itemType, itemSize); + mDevice = device; } - protected boolean isPebbleHealthEnabled() { + public GBDevice getDevice() { + return mDevice; + } + + boolean isPebbleHealthEnabled() { Prefs prefs = GBApplication.getPrefs(); return prefs.getBoolean("pebble_sync_health", true); } + + boolean storePebbleHealthRawRecord() { + Prefs prefs = GBApplication.getPrefs(); + return prefs.getBoolean("pebble_health_store_raw", true); + } } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java index fcc19641..4e895849 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java @@ -9,6 +9,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.ParcelUuid; +import android.support.v4.content.LocalBroadcastManager; import org.json.JSONArray; import org.json.JSONException; @@ -20,6 +21,8 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; import java.net.InetAddress; import java.net.Socket; import java.nio.ByteBuffer; @@ -28,6 +31,8 @@ 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.activities.appmanager.AppManagerActivity; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppManagement; @@ -37,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PBWReader; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleInstallable; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; +import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.ble.PebbleLESupport; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; @@ -44,7 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -public class PebbleIoThread extends GBDeviceIoThread { +class PebbleIoThread extends GBDeviceIoThread { private static final Logger LOG = LoggerFactory.getLogger(PebbleIoThread.class); public static final String PEBBLEKIT_ACTION_PEBBLE_CONNECTED = "com.getpebble.action.PEBBLE_CONNECTED"; @@ -58,7 +64,7 @@ public class PebbleIoThread extends GBDeviceIoThread { public static final String PEBBLEKIT_ACTION_APP_START = "com.getpebble.action.app.START"; public static final String PEBBLEKIT_ACTION_APP_STOP = "com.getpebble.action.app.STOP"; - final Prefs prefs = GBApplication.getPrefs(); + private final Prefs prefs = GBApplication.getPrefs(); private final PebbleProtocol mPebbleProtocol; private final PebbleSupport mPebbleSupport; @@ -70,11 +76,14 @@ public class PebbleIoThread extends GBDeviceIoThread { private Socket mTCPSocket = null; // for emulator private InputStream mInStream = null; private OutputStream mOutStream = null; + private PebbleLESupport mPebbleLESupport; + private boolean mQuit = false; private boolean mIsConnected = false; private boolean mIsInstalling = false; private PBWReader mPBWReader = null; + private GBDeviceApp mCurrentlyInstallingApp = null; private int mAppInstallToken = -1; private InputStream mFis = null; private PebbleAppInstallState mInstallState = PebbleAppInstallState.UNKNOWN; @@ -140,17 +149,7 @@ public class PebbleIoThread extends GBDeviceIoThread { getContext().sendBroadcast(intent); } - private void sendAppMessageAck(int transactionId) { - if (transactionId > 0 && transactionId <= 255) { - Intent intent = new Intent(); - intent.setAction(PEBBLEKIT_ACTION_APP_RECEIVE_ACK); - intent.putExtra("transaction_id", transactionId); - LOG.info("broadcasting ACK (transaction id " + transactionId + ")"); - getContext().sendBroadcast(intent); - } - } - - public PebbleIoThread(PebbleSupport pebbleSupport, GBDevice gbDevice, GBDeviceProtocol gbDeviceProtocol, BluetoothAdapter btAdapter, Context context) { + PebbleIoThread(PebbleSupport pebbleSupport, GBDevice gbDevice, GBDeviceProtocol gbDeviceProtocol, BluetoothAdapter btAdapter, Context context) { super(gbDevice, context); mPebbleProtocol = (PebbleProtocol) gbDeviceProtocol; mBtAdapter = btAdapter; @@ -158,34 +157,61 @@ public class PebbleIoThread extends GBDeviceIoThread { mEnablePebblekit = prefs.getBoolean("pebble_enable_pebblekit", false); } + private int readWithException(InputStream inputStream, byte[] buffer, int byteOffset, int byteCount) throws IOException { + int ret = inputStream.read(buffer, byteOffset, byteCount); + if (ret == -1) { + throw new IOException("broken pipe"); + } + return ret; + } + + private void sendAppMessageAck(int transactionId) { + Intent intent = new Intent(); + intent.setAction(PEBBLEKIT_ACTION_APP_RECEIVE_ACK); + intent.putExtra("transaction_id", transactionId); + LOG.info("broadcasting ACK (transaction id " + transactionId + ")"); + getContext().sendBroadcast(intent); + } + @Override - protected boolean connect(String btDeviceAddress) { + protected boolean connect() { + String deviceAddress = gbDevice.getAddress(); GBDevice.State originalState = gbDevice.getState(); gbDevice.setState(GBDevice.State.CONNECTING); gbDevice.sendDeviceUpdateIntent(getContext()); try { // contains only one ":"? then it is addr:port - int firstColon = btDeviceAddress.indexOf(":"); - if (firstColon == btDeviceAddress.lastIndexOf(":")) { + int firstColon = deviceAddress.indexOf(":"); + if (firstColon == deviceAddress.lastIndexOf(":")) { mIsTCP = true; - InetAddress serverAddr = InetAddress.getByName(btDeviceAddress.substring(0, firstColon)); - mTCPSocket = new Socket(serverAddr, Integer.parseInt(btDeviceAddress.substring(firstColon + 1))); + InetAddress serverAddr = InetAddress.getByName(deviceAddress.substring(0, firstColon)); + mTCPSocket = new Socket(serverAddr, Integer.parseInt(deviceAddress.substring(firstColon + 1))); mInStream = mTCPSocket.getInputStream(); mOutStream = mTCPSocket.getOutputStream(); } else { mIsTCP = false; - BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(btDeviceAddress); - ParcelUuid uuids[] = btDevice.getUuids(); - if (uuids == null) { - return false; + if (gbDevice.getVolatileAddress() != null && prefs.getBoolean("pebble_force_le", false)) { + deviceAddress = gbDevice.getVolatileAddress(); } - for (ParcelUuid uuid : uuids) { - LOG.info("found service UUID " + uuid); + BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(deviceAddress); + if (btDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE) { + LOG.info("This is a Pebble 2 or Pebble-LE/Pebble Time LE, will use BLE"); + mInStream = new PipedInputStream(); + mOutStream = new PipedOutputStream(); + mPebbleLESupport = new PebbleLESupport(this.getContext(), btDevice, (PipedInputStream) mInStream, (PipedOutputStream) mOutStream); + } else { + ParcelUuid uuids[] = btDevice.getUuids(); + if (uuids == null) { + return false; + } + for (ParcelUuid uuid : uuids) { + LOG.info("found service UUID " + uuid); + } + mBtSocket = btDevice.createRfcommSocketToServiceRecord(uuids[0].getUuid()); + mBtSocket.connect(); + mInStream = mBtSocket.getInputStream(); + mOutStream = mBtSocket.getOutputStream(); } - mBtSocket = btDevice.createRfcommSocketToServiceRecord(uuids[0].getUuid()); - mBtSocket.connect(); - mInStream = mBtSocket.getInputStream(); - mOutStream = mBtSocket.getOutputStream(); } } catch (IOException e) { e.printStackTrace(); @@ -210,9 +236,9 @@ public class PebbleIoThread extends GBDeviceIoThread { @Override public void run() { - mIsConnected = connect(gbDevice.getAddress()); + mIsConnected = connect(); if (!mIsConnected) { - if (GBApplication.getGBPrefs().getAutoReconnect()) { + if (GBApplication.getGBPrefs().getAutoReconnect() && !mQuit) { gbDevice.setState(GBDevice.State.WAITING_FOR_RECONNECT); gbDevice.sendDeviceUpdateIntent(getContext()); } @@ -296,8 +322,8 @@ public class PebbleIoThread extends GBDeviceIoThread { if (mPBWReader.isFirmware()) { writeInstallApp(mPebbleProtocol.encodeInstallFirmwareComplete()); finishInstall(false); - } else if (mPBWReader.isLanguage() || mPebbleProtocol.isFw3x) { - finishInstall(false); // FIXME: dont know yet how to detect success + } else if (mPBWReader.isLanguage() || mPebbleProtocol.mFwMajor >= 3) { + finishInstall(false); // FIXME: don't know yet how to detect success } else { writeInstallApp(mPebbleProtocol.encodeAppRefresh(mInstallSlot)); } @@ -309,11 +335,12 @@ public class PebbleIoThread extends GBDeviceIoThread { if (mIsTCP) { mInStream.skip(6); } - int bytes = mInStream.read(buffer, 0, 4); + int bytes = readWithException(mInStream, buffer, 0, 4); - if (bytes < 4) { - continue; + while (bytes < 4) { + bytes += readWithException(mInStream, buffer, bytes, 4 - bytes); } + ByteBuffer buf = ByteBuffer.wrap(buffer); buf.order(ByteOrder.BIG_ENDIAN); short length = buf.getShort(); @@ -321,19 +348,14 @@ public class PebbleIoThread extends GBDeviceIoThread { if (length < 0 || length > 8192) { LOG.info("invalid length " + length); while (mInStream.available() > 0) { - mInStream.read(buffer); // read all + readWithException(mInStream, buffer, 0, buffer.length); // read all } continue; } - bytes = mInStream.read(buffer, 4, length); + bytes = readWithException(mInStream, buffer, 4, length); while (bytes < length) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - bytes += mInStream.read(buffer, bytes + 4, length - bytes); + bytes += readWithException(mInStream, buffer, bytes + 4, length - bytes); } if (mIsTCP) { @@ -359,7 +381,7 @@ public class PebbleIoThread extends GBDeviceIoThread { e.printStackTrace(); } } catch (IOException e) { - if (e.getMessage().contains("socket closed")) { //FIXME: this does not feel right + if (e.getMessage() != null && (e.getMessage().equals("broken pipe") || e.getMessage().contains("socket closed"))) { //FIXME: this does not feel right LOG.info(e.getMessage()); mIsConnected = false; int reconnectAttempts = prefs.getInt("pebble_reconnect_attempts", 10); @@ -370,7 +392,7 @@ public class PebbleIoThread extends GBDeviceIoThread { int delaySeconds = 1; while (reconnectAttempts-- > 0 && !mQuit && !mIsConnected) { LOG.info("Trying to reconnect (attempts left " + reconnectAttempts + ")"); - mIsConnected = connect(gbDevice.getAddress()); + mIsConnected = connect(); if (!mIsConnected) { try { Thread.sleep(delaySeconds * 1000); @@ -451,7 +473,11 @@ public class PebbleIoThread extends GBDeviceIoThread { mOutStream.flush(); } } catch (IOException e) { - LOG.error("Error writing.", e); + LOG.error("Error writing.", e.getMessage()); + } + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { } } @@ -474,6 +500,7 @@ public class PebbleIoThread extends GBDeviceIoThread { LOG.info("syncing time"); write(mPebbleProtocol.encodeSetTime()); } + write(mPebbleProtocol.encodeEnableAppLogs(prefs.getBoolean("pebble_enable_applogs",false))); write(mPebbleProtocol.encodeReportDataLogSessions()); gbDevice.setState(GBDevice.State.INITIALIZED); return false; @@ -554,11 +581,11 @@ public class PebbleIoThread extends GBDeviceIoThread { return false; } - public void setToken(int token) { + private void setToken(int token) { mAppInstallToken = token; } - public void setInstallSlot(int slot) { + private void setInstallSlot(int slot) { if (mIsInstalling) { mInstallSlot = slot; } @@ -572,7 +599,7 @@ public class PebbleIoThread extends GBDeviceIoThread { write_real(bytes); } - public void installApp(Uri uri, int appId) { + void installApp(Uri uri, int appId) { if (mIsInstalling) { return; } @@ -582,8 +609,12 @@ public class PebbleIoThread extends GBDeviceIoThread { write(mPebbleProtocol.encodeSetSaneDistanceUnit(true)); return; } + if (uri.equals(Uri.parse("fake://hrm"))) { + write(mPebbleProtocol.encodeActivateHRM(true)); + return; + } - String platformName = PebbleUtils.getPlatformName(gbDevice.getHardwareVersion()); + String platformName = PebbleUtils.getPlatformName(gbDevice.getModel()); try { mPBWReader = new PBWReader(uri, getContext(), platformName); @@ -613,12 +644,12 @@ public class PebbleIoThread extends GBDeviceIoThread { */ writeInstallApp(mPebbleProtocol.encodeGetTime()); } else { - GBDeviceApp app = mPBWReader.getGBDeviceApp(); - if (mPebbleProtocol.isFw3x && !mPBWReader.isLanguage()) { + mCurrentlyInstallingApp = mPBWReader.getGBDeviceApp(); + if (mPebbleProtocol.mFwMajor >= 3 && !mPBWReader.isLanguage()) { if (appId == 0) { // only install metadata - not the binaries - write(mPebbleProtocol.encodeInstallMetadata(app.getUUID(), app.getName(), mPBWReader.getAppVersion(), mPBWReader.getSdkVersion(), mPBWReader.getFlags(), mPBWReader.getIconId())); - write(mPebbleProtocol.encodeAppStart(app.getUUID(), true)); + write(mPebbleProtocol.encodeInstallMetadata(mCurrentlyInstallingApp.getUUID(), mCurrentlyInstallingApp.getName(), mPBWReader.getAppVersion(), mPBWReader.getSdkVersion(), mPBWReader.getFlags(), mPBWReader.getIconId())); + write(mPebbleProtocol.encodeAppStart(mCurrentlyInstallingApp.getUUID(), true)); } else { // this came from an app fetch request, so do the real stuff mIsInstalling = true; @@ -637,13 +668,13 @@ public class PebbleIoThread extends GBDeviceIoThread { writeInstallApp(mPebbleProtocol.encodeGetTime()); } else { mInstallState = PebbleAppInstallState.WAIT_SLOT; - writeInstallApp(mPebbleProtocol.encodeAppDelete(app.getUUID())); + writeInstallApp(mPebbleProtocol.encodeAppDelete(mCurrentlyInstallingApp.getUUID())); } } } } - public void finishInstall(boolean hadError) { + private void finishInstall(boolean hadError) { if (!mIsInstalling) { return; } @@ -651,6 +682,19 @@ public class PebbleIoThread extends GBDeviceIoThread { GB.updateInstallNotification(getContext().getString(R.string.installation_failed_), false, 0, getContext()); } else { GB.updateInstallNotification(getContext().getString(R.string.installation_successful), false, 0, getContext()); + if (mPebbleProtocol.mFwMajor >= 3) { + String filenameSuffix; + if (mCurrentlyInstallingApp != null) { + if (mCurrentlyInstallingApp.getType() == GBDeviceApp.Type.WATCHFACE) { + filenameSuffix = ".watchfaces"; + } else { + filenameSuffix = ".watchapps"; + } + AppManagerActivity.addToAppOrderFile(gbDevice.getAddress() + filenameSuffix, mCurrentlyInstallingApp.getUUID()); + Intent refreshIntent = new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(refreshIntent); + } + } } mInstallState = PebbleAppInstallState.UNKNOWN; @@ -660,6 +704,8 @@ public class PebbleIoThread extends GBDeviceIoThread { mPBWReader = null; mIsInstalling = false; + mCurrentlyInstallingApp = null; + if (mFis != null) { try { mFis.close(); @@ -678,16 +724,20 @@ public class PebbleIoThread extends GBDeviceIoThread { if (mBtSocket != null) { try { mBtSocket.close(); - } catch (IOException e) { - e.printStackTrace(); + } catch (IOException ignored) { } + mBtSocket = null; } if (mTCPSocket != null) { try { mTCPSocket.close(); - } catch (IOException e) { - e.printStackTrace(); + } catch (IOException ignored) { } + mTCPSocket = null; + } + if (mPebbleLESupport != null) { + mPebbleLESupport.close(); + mPebbleLESupport = null; } } @@ -702,4 +752,4 @@ public class PebbleIoThread extends GBDeviceIoThread { UPLOAD_COMPLETE, APP_REFRESH, } -} \ No newline at end of file +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index 30707041..e79bd37b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -18,7 +18,6 @@ import java.util.Random; import java.util.SimpleTimeZone; import java.util.UUID; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppManagement; @@ -29,12 +28,13 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificati import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; -import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleColor; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleIconID; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; @@ -69,6 +69,8 @@ public class PebbleProtocol extends GBDeviceProtocol { public static final short ENDPOINT_DATALOG = 6778; static final short ENDPOINT_RUNKEEPER = 7000; static final short ENDPOINT_SCREENSHOT = 8000; + static final short ENDPOINT_AUDIOSTREAM = 10000; + static final short ENDPOINT_VOICECONTROL = 11000; static final short ENDPOINT_NOTIFICATIONACTION = 11440; // 3.x only, TODO: find a better name static final short ENDPOINT_APPREORDER = (short) 0xabcd; // 3.x only static final short ENDPOINT_BLOBDB = (short) 45531; // 3.x only @@ -85,7 +87,10 @@ public class PebbleProtocol extends GBDeviceProtocol { static final byte BLOBDB_APP = 2; static final byte BLOBDB_REMINDER = 3; static final byte BLOBDB_NOTIFICATION = 4; + static final byte BLOBDB_CANNED_MESSAGES = 6; static final byte BLOBDB_PREFERENCES = 7; + static final byte BLOBDB_APPGLANCE = 11; + static final byte BLOBDB_SUCCESS = 1; static final byte BLOBDB_GENERALFAILURE = 2; static final byte BLOBDB_INVALIDOPERATION = 3; @@ -245,20 +250,24 @@ public class PebbleProtocol extends GBDeviceProtocol { static final long GB_UUID_MASK = 0x4767744272646700L; - // base is -5 + // base is -8 private static final String[] hwRevisions = { // Emulator - "spalding_bb2", "snowy_bb2", "snowy_bb", "bb2", "bb", + "silk_bb2", "robert_bb", "silk_bb", + "spalding_bb2", "snowy_bb2", "snowy_bb", + "bb2", "bb", "unknown", - // Pebble + // Pebble Classic Series "ev1", "ev2", "ev2_3", "ev2_4", "v1_5", "v2_0", - // Pebble Time - "snowy_evt2", "snowy_dvt", "spalding_dvt", "snowy_s3", "spalding" + // Pebble Time Series + "snowy_evt2", "snowy_dvt", "spalding_dvt", "snowy_s3", "spalding", + // Pebble 2 Series + "silk_evt", "robert_evt", "silk" }; private static final Random mRandom = new Random(); - boolean isFw3x = false; + int mFwMajor = 3; boolean mForceProtocol = false; GBDeviceEventScreenshot mDevEventScreenshot = null; int mScreenshotRemaining = -1; @@ -358,6 +367,7 @@ public class PebbleProtocol extends GBDeviceProtocol { private final ArrayList tmpUUIDS = new ArrayList<>(); public static final UUID UUID_PEBBLE_HEALTH = UUID.fromString("36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c"); // FIXME: store somewhere else, this is also accessed by other code + public static final UUID UUID_WORKOUT = UUID.fromString("fef82c82-7176-4e22-88de-35a3fc18d43f"); // FIXME: store somewhere else, this is also accessed by other code private static final UUID UUID_GBPEBBLE = UUID.fromString("61476764-7465-7262-6469-656775527a6c"); private static final UUID UUID_MORPHEUZ = UUID.fromString("5be44f1d-d262-4ea6-aa30-ddbec1e3cab2"); private static final UUID UUID_WHETHERNEAT = UUID.fromString("3684003b-a685-45f9-a713-abc6364ba051"); @@ -368,10 +378,10 @@ public class PebbleProtocol extends GBDeviceProtocol { private static final UUID UUID_ZERO = new UUID(0, 0); - private static final Map mAppMessageHandlers = new HashMap<>(); + private final Map mAppMessageHandlers = new HashMap<>(); - { - mAppMessageHandlers.put(UUID_GBPEBBLE, new AppMessageHandlerGBPebble(UUID_GBPEBBLE, PebbleProtocol.this)); + public PebbleProtocol(GBDevice device) { + super(device); mAppMessageHandlers.put(UUID_MORPHEUZ, new AppMessageHandlerMorpheuz(UUID_MORPHEUZ, PebbleProtocol.this)); mAppMessageHandlers.put(UUID_WHETHERNEAT, new AppMessageHandlerWeatherNeat(UUID_WHETHERNEAT, PebbleProtocol.this)); mAppMessageHandlers.put(UUID_MISFIT, new AppMessageHandlerMisfit(UUID_MISFIT, PebbleProtocol.this)); @@ -382,7 +392,7 @@ public class PebbleProtocol extends GBDeviceProtocol { private final HashMap mDatalogSessions = new HashMap<>(); - private static byte[] encodeSimpleMessage(short endpoint, byte command) { + private byte[] encodeSimpleMessage(short endpoint, byte command) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_SIMPLEMESSAGE); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort(LENGTH_SIMPLEMESSAGE); @@ -451,15 +461,15 @@ public class PebbleProtocol extends GBDeviceProtocol { } Long ts = System.currentTimeMillis(); - if (!isFw3x) { + if (mFwMajor < 3) { ts += (SimpleTimeZone.getDefault().getOffset(ts)); } ts /= 1000; - if (isFw3x) { + if (mFwMajor >= 3) { // 3.x notification return encodeBlobdbNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body, notificationSpec.sourceName, hasHandle, notificationSpec.type, notificationSpec.cannedReplies); - } else if (mForceProtocol || notificationSpec.type != NotificationType.EMAIL) { + } else if (mForceProtocol || notificationSpec.type != NotificationType.GENERIC_EMAIL) { // 2.x notification return encodeExtensibleNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body, notificationSpec.sourceName, hasHandle, notificationSpec.cannedReplies); } else { @@ -498,7 +508,7 @@ public class PebbleProtocol extends GBDeviceProtocol { long ts = System.currentTimeMillis(); long ts_offset = (SimpleTimeZone.getDefault().getOffset(ts)); ByteBuffer buf; - if (isFw3x) { + if (mFwMajor >= 3) { String timezone = SimpleTimeZone.getDefault().getID(); short length = (short) (LENGTH_SETTIME + timezone.getBytes().length + 3); buf = ByteBuffer.allocate(LENGTH_PREFIX + length); @@ -527,7 +537,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return encodeSetCallState("Where are you?", "Gadgetbridge", start ? CallSpec.CALL_INCOMING : CallSpec.CALL_END); } - private static byte[] encodeExtensibleNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName, boolean hasHandle, String[] cannedReplies) { + private byte[] encodeExtensibleNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName, boolean hasHandle, String[] cannedReplies) { final short ACTION_LENGTH_MIN = 10; String[] parts = {title, subtitle, body}; @@ -715,33 +725,31 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - public byte[] encodeActivateHealth(boolean activate) { + byte[] encodeActivateHealth(boolean activate) { byte[] blob; - byte command; - command = BLOBDB_INSERT; if (activate) { ByteBuffer buf = ByteBuffer.allocate(9); buf.order(ByteOrder.LITTLE_ENDIAN); ActivityUser activityUser = new ActivityUser(); - Integer heightMm = activityUser.getActivityUserHeightCm() * 10; + Integer heightMm = activityUser.getHeightCm() * 10; buf.putShort(heightMm.shortValue()); - Integer weigthDag = activityUser.getActivityUserWeightKg() * 100; + Integer weigthDag = activityUser.getWeightKg() * 100; buf.putShort(weigthDag.shortValue()); buf.put((byte) 0x01); //activate tracking buf.put((byte) 0x00); //activity Insights buf.put((byte) 0x00); //sleep Insights - buf.put((byte) activityUser.getActivityUserAge()); - buf.put((byte) activityUser.getActivityUserGender()); + buf.put((byte) activityUser.getAge()); + buf.put((byte) activityUser.getGender()); blob = buf.array(); } else { blob = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; } - return encodeBlobdb("activityPreferences", command, BLOBDB_PREFERENCES, blob); + return encodeBlobdb("activityPreferences", BLOBDB_INSERT, BLOBDB_PREFERENCES, blob); } - public byte[] encodeSetSaneDistanceUnit(boolean sane) { + byte[] encodeSetSaneDistanceUnit(boolean sane) { byte value; if (sane) { value = 0x00; @@ -751,7 +759,13 @@ public class PebbleProtocol extends GBDeviceProtocol { return encodeBlobdb("unitsDistance", BLOBDB_INSERT, BLOBDB_PREFERENCES, new byte[]{value}); } - public byte[] encodeReportDataLogSessions() { + + byte[] encodeActivateHRM(boolean activate) { + return encodeBlobdb("hrmPreferences", BLOBDB_INSERT, BLOBDB_PREFERENCES, + activate ? new byte[]{0x01} : new byte[]{0x00}); + } + + byte[] encodeReportDataLogSessions() { return encodeSimpleMessage(ENDPOINT_DATALOG, DATALOG_REPORTSESSIONS); } @@ -798,7 +812,7 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.putShort(duration); buf.put((byte) 0x02); // type (0x02 = pin) buf.putShort((short) 0x0001); // flags 0x0001 = ? - buf.put((byte) 0x01); // layout was (0x02 = pin?), 0x01 needed for subtitle aber seems to do no harm if there isn't one + buf.put((byte) 0x01); // layout was (0x02 = pin?), 0x01 needed for subtitle but seems to do no harm if there isn't one buf.putShort((short) attributes_length); // total length of all attributes and actions in bytes buf.put(attributes_count); @@ -816,7 +830,6 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.put(subtitle.getBytes()); } - return encodeBlobdb(uuid, BLOBDB_INSERT, BLOBDB_PIN, buf.array()); } @@ -826,34 +839,13 @@ public class PebbleProtocol extends GBDeviceProtocol { String[] parts = {title, subtitle, body}; - int icon_id; - byte color_id; - switch (notificationType) { - case EMAIL: - icon_id = PebbleIconID.GENERIC_EMAIL; - color_id = PebbleColor.JaegerGreen; - break; - case SMS: - icon_id = PebbleIconID.GENERIC_SMS; - color_id = PebbleColor.VividViolet; - break; - case TWITTER: - icon_id = PebbleIconID.NOTIFICATION_TWITTER; - color_id = PebbleColor.BlueMoon; - break; - case FACEBOOK: - icon_id = PebbleIconID.NOTIFICATION_FACEBOOK; - color_id = PebbleColor.VeryLightBlue; - break; - case CHAT: - icon_id = PebbleIconID.NOTIFICATION_HIPCHAT; - color_id = PebbleColor.Inchworm; - break; - default: - icon_id = PebbleIconID.NOTIFICATION_GENERIC; - color_id = PebbleColor.Red; - break; + if(notificationType == null) { + notificationType = NotificationType.UNKNOWN; } + + int icon_id = notificationType.icon; + byte color_id = notificationType.color; + // Calculate length first byte actions_count; short actions_length; @@ -992,7 +984,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return encodeBlobdb(UUID.randomUUID(), BLOBDB_INSERT, BLOBDB_NOTIFICATION, buf.array()); } - public byte[] encodeActionResponse2x(int id, byte actionId, int iconId, String caption) { + private byte[] encodeActionResponse2x(int id, byte actionId, int iconId, String caption) { short length = (short) (18 + caption.getBytes().length); ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length); buf.order(ByteOrder.BIG_ENDIAN); @@ -1013,7 +1005,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - public byte[] encodeActionResponse(UUID uuid, int iconId, String caption) { + private byte[] encodeActionResponse(UUID uuid, int iconId, String caption) { short length = (short) (29 + caption.getBytes().length); ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length); buf.order(ByteOrder.BIG_ENDIAN); @@ -1034,7 +1026,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - public byte[] encodeInstallMetadata(UUID uuid, String appName, short appVersion, short sdkVersion, int flags, int iconId) { + byte[] encodeInstallMetadata(UUID uuid, String appName, short appVersion, short sdkVersion, int flags, int iconId) { final short METADATA_LENGTH = 126; byte[] name_buf = new byte[96]; @@ -1067,7 +1059,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - public byte[] encodeGetTime() { + byte[] encodeGetTime() { return encodeSimpleMessage(ENDPOINT_TIME, TIME_GETTIME); } @@ -1106,6 +1098,10 @@ public class PebbleProtocol extends GBDeviceProtocol { } public byte[] encodeSetMusicState(byte state, int position, int playRate, byte shuffle, byte repeat) { + if (mFwMajor < 3) { + return null; + } + byte playState; switch (state) { @@ -1130,7 +1126,7 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.order(ByteOrder.LITTLE_ENDIAN); buf.put(MUSICCONTROL_SETPLAYSTATE); buf.put(playState); - buf.putInt(position); + buf.putInt(position * 1000); buf.putInt(playRate); buf.put(shuffle); buf.put(repeat); @@ -1141,7 +1137,7 @@ public class PebbleProtocol extends GBDeviceProtocol { @Override public byte[] encodeSetMusicInfo(String artist, String album, String track, int duration, int trackCount, int trackNr) { String[] parts = {artist, album, track}; - if (duration == 0) { + if (duration == 0 || mFwMajor < 3) { return encodeMessage(ENDPOINT_MUSICCONTROL, MUSICCONTROL_SETMUSICINFO, 0, parts); } else { // Calculate length first @@ -1192,7 +1188,7 @@ public class PebbleProtocol extends GBDeviceProtocol { @Override public byte[] encodeAppInfoReq() { - if (isFw3x) { + if (mFwMajor >= 3) { return null; // can't do this on 3.x :( } return encodeSimpleMessage(ENDPOINT_APPMANAGER, APPMANAGER_GETUUIDS); @@ -1200,7 +1196,7 @@ public class PebbleProtocol extends GBDeviceProtocol { @Override public byte[] encodeAppStart(UUID uuid, boolean start) { - if (isFw3x) { + if (mFwMajor >= 3) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_APPRUNSTATE); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort(LENGTH_APPRUNSTATE); @@ -1219,10 +1215,13 @@ public class PebbleProtocol extends GBDeviceProtocol { @Override public byte[] encodeAppDelete(UUID uuid) { - if (isFw3x) { + if (mFwMajor >= 3) { if (UUID_PEBBLE_HEALTH.equals(uuid)) { return encodeActivateHealth(false); } + if (UUID_WORKOUT.equals(uuid)) { + return encodeActivateHRM(false); + } return encodeBlobdb(uuid, BLOBDB_DELETE, BLOBDB_APP, null); } else { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_REMOVEAPP_2X); @@ -1272,16 +1271,16 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.putInt(os); buf.put(PHONEVERSION_APPVERSION_MAGIC); - buf.put((byte) 3); // major - buf.put((byte) 12); // minor - buf.put((byte) 0); // patch + buf.put((byte) 4); // major + buf.put((byte) 1); // minor + buf.put((byte) 1); // patch buf.order(ByteOrder.LITTLE_ENDIAN); - buf.putLong(0x00000000000001af); //flags + buf.putLong(0x00000000000029af); //flags return buf.array(); } - public byte[] encodePhoneVersion(byte os) { + private byte[] encodePhoneVersion(byte os) { return encodePhoneVersion3x(os); } @@ -1295,7 +1294,8 @@ public class PebbleProtocol extends GBDeviceProtocol { return encodeSimpleMessage(ENDPOINT_SCREENSHOT, SCREENSHOT_TAKE); } - public byte[] encodeAppReoder(UUID[] uuids) { + @Override + public byte[] encodeAppReorder(UUID[] uuids) { int length = 2 + uuids.length * LENGTH_UUID; ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length); buf.order(ByteOrder.BIG_ENDIAN); @@ -1311,10 +1311,57 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } + @Override + public byte[] encodeSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + + if (cannedMessagesSpec.cannedMessages == null || cannedMessagesSpec.cannedMessages.length == 0) { + return null; + } + + String blobDBKey; + switch (cannedMessagesSpec.type) { + case CannedMessagesSpec.TYPE_MISSEDCALLS: + blobDBKey = "com.pebble.android.phone"; + break; + case CannedMessagesSpec.TYPE_NEWSMS: + blobDBKey = "com.pebble.sendText"; + break; + default: + return null; + } + + int replies_length = -1; + + for (String reply : cannedMessagesSpec.cannedMessages) { + replies_length += reply.getBytes().length + 1; + } + + ByteBuffer buf = ByteBuffer.allocate(12 + replies_length); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(0x00000000); // unknown + buf.put((byte) 0x00); // attributes count? + buf.put((byte) 0x01); // actions count? + + // action + buf.put((byte) 0x00); // action id + buf.put((byte) 0x03); // action type = reply + buf.put((byte) 0x01); // attributes count + buf.put((byte) 0x08); // canned messages + buf.putShort((short) replies_length); + for (int i = 0; i < cannedMessagesSpec.cannedMessages.length - 1; i++) { + buf.put(cannedMessagesSpec.cannedMessages[i].getBytes()); + buf.put((byte) 0x00); + } + // last one must not be zero terminated, else we get an additional empty reply + buf.put(cannedMessagesSpec.cannedMessages[cannedMessagesSpec.cannedMessages.length - 1].getBytes()); + + return encodeBlobdb(blobDBKey, BLOBDB_INSERT, BLOBDB_CANNED_MESSAGES, buf.array()); + } + /* pebble specific install methods */ - public byte[] encodeUploadStart(byte type, int app_id, int size, String filename) { + byte[] encodeUploadStart(byte type, int app_id, int size, String filename) { short length; - if (isFw3x && (type != PUTBYTES_TYPE_FILE)) { + if (mFwMajor >= 3 && (type != PUTBYTES_TYPE_FILE)) { length = LENGTH_UPLOADSTART_3X; type |= 0b10000000; } else { @@ -1333,7 +1380,7 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.putInt(size); buf.put(type); - if (isFw3x && (type != PUTBYTES_TYPE_FILE)) { + if (mFwMajor >= 3 && (type != PUTBYTES_TYPE_FILE)) { buf.putInt(app_id); } else { // slot @@ -1348,7 +1395,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - public byte[] encodeUploadChunk(int token, byte[] buffer, int size) { + byte[] encodeUploadChunk(int token, byte[] buffer, int size) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_UPLOADCHUNK + size); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort((short) (LENGTH_UPLOADCHUNK + size)); @@ -1360,7 +1407,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - public byte[] encodeUploadCommit(int token, int crc) { + byte[] encodeUploadCommit(int token, int crc) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_UPLOADCOMMIT); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort(LENGTH_UPLOADCOMMIT); @@ -1371,7 +1418,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - public byte[] encodeUploadComplete(int token) { + byte[] encodeUploadComplete(int token) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_UPLOADCOMPLETE); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort(LENGTH_UPLOADCOMPLETE); @@ -1381,7 +1428,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - public byte[] encodeUploadCancel(int token) { + byte[] encodeUploadCancel(int token) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_UPLOADCANCEL); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort(LENGTH_UPLOADCANCEL); @@ -1402,11 +1449,11 @@ public class PebbleProtocol extends GBDeviceProtocol { } - public byte[] encodeInstallFirmwareStart() { + byte[] encodeInstallFirmwareStart() { return encodeSystemMessage(SYSTEMMESSAGE_FIRMWARESTART); } - public byte[] encodeInstallFirmwareComplete() { + byte[] encodeInstallFirmwareComplete() { return encodeSystemMessage(SYSTEMMESSAGE_FIRMWARECOMPLETE); } @@ -1415,7 +1462,7 @@ public class PebbleProtocol extends GBDeviceProtocol { } - public byte[] encodeAppRefresh(int index) { + byte[] encodeAppRefresh(int index) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_REFRESHAPP); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort(LENGTH_REFRESHAPP); @@ -1426,7 +1473,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - public byte[] encodeDatalog(byte handle, byte reply) { + private byte[] encodeDatalog(byte handle, byte reply) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + 2); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort((short) 2); @@ -1451,7 +1498,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - private static byte[] encodePing(byte command, int cookie) { + private byte[] encodePing(byte command, int cookie) { ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_PING); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort(LENGTH_PING); @@ -1462,6 +1509,17 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } + byte[] encodeEnableAppLogs(boolean enable) { + final short LENGTH_APPLOGS = 1; + ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_APPLOGS); + buf.order(ByteOrder.BIG_ENDIAN); + buf.putShort(LENGTH_APPLOGS); + buf.putShort(ENDPOINT_APPLOGS); + buf.put((byte) (enable ? 1 : 0)); + + return buf.array(); + } + private ArrayList> decodeDict(ByteBuffer buf) { ArrayList> dict = new ArrayList<>(); buf.order(ByteOrder.LITTLE_ENDIAN); @@ -1571,6 +1629,8 @@ public class PebbleProtocol extends GBDeviceProtocol { length += ((String) pair.second).getBytes().length + 1; } else if (pair.second instanceof byte[]) { length += ((byte[]) pair.second).length; + } else { + LOG.warn("unknown type: " + pair.second.getClass().toString()); } } @@ -1616,7 +1676,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return buf.array(); } - public byte[] encodeApplicationMessageFromJSON(UUID uuid, JSONArray jsonArray) { + byte[] encodeApplicationMessageFromJSON(UUID uuid, JSONArray jsonArray) { ArrayList> pairs = new ArrayList<>(); for (int i = 0; i < jsonArray.length(); i++) { try { @@ -1655,7 +1715,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return encodeApplicationMessagePush(ENDPOINT_APPLICATIONMESSAGE, uuid, pairs); } - private static byte reverseBits(byte in) { + private byte reverseBits(byte in) { byte out = 0; for (int i = 0; i < 8; i++) { byte bit = (byte) (in & 1); @@ -1719,19 +1779,15 @@ public class PebbleProtocol extends GBDeviceProtocol { byte command = buf.get(); if (command == NOTIFICATIONACTION_INVOKE) { int id; - long uuid_high = 0; - long uuid_low = 0; - if (isFw3x) { - buf.order(ByteOrder.BIG_ENDIAN); - uuid_high = buf.getLong(); - uuid_low = buf.getLong(); - buf.order(ByteOrder.LITTLE_ENDIAN); - id = (int) (uuid_low & 0xffffffffL); + UUID uuid = new UUID(0,0); + if (mFwMajor >= 3) { + uuid = getUUID(buf); + id = (int) (uuid.getLeastSignificantBits() & 0xffffffffL); } else { id = buf.getInt(); } byte action = buf.get(); - if (action >= 0x01 && action <= 0x05) { + if (action >= 0x00 && action <= 0x05) { GBDeviceEventNotificationControl devEvtNotificationControl = new GBDeviceEventNotificationControl(); devEvtNotificationControl.handle = id; String caption = "undefined"; @@ -1761,6 +1817,7 @@ public class PebbleProtocol extends GBDeviceProtocol { icon_id = PebbleIconID.RESULT_MUTE; break; case 0x05: + case 0x00: boolean failed = true; byte attribute_count = buf.get(); if (attribute_count > 0) { @@ -1770,15 +1827,18 @@ public class PebbleProtocol extends GBDeviceProtocol { if (length > 64) length = 64; byte[] reply = new byte[length]; buf.get(reply); - // FIXME: this does not belong here, but we want at least check if there is no chance at all to send out the SMS later before we report success - String phoneNumber = (String) GBApplication.getIDSenderLookup().lookup(id); - //if (phoneNumber != null) { + devEvtNotificationControl.phoneNumber = null; + if (buf.remaining() > 1 && buf.get() == 0x0c) { + short phoneNumberLength = buf.getShort(); + byte[] phoneNumberBytes = new byte[phoneNumberLength]; + buf.get(phoneNumberBytes); + devEvtNotificationControl.phoneNumber = new String(phoneNumberBytes); + } devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY; devEvtNotificationControl.reply = new String(reply); caption = "SENT"; icon_id = PebbleIconID.RESULT_SENT; failed = false; - //} } } if (failed) { @@ -1789,10 +1849,10 @@ public class PebbleProtocol extends GBDeviceProtocol { break; } GBDeviceEventSendBytes sendBytesAck = null; - if (isFw3x || needsAck2x) { + if (mFwMajor >= 3 || needsAck2x) { sendBytesAck = new GBDeviceEventSendBytes(); - if (isFw3x) { - sendBytesAck.encodedBytes = encodeActionResponse(new UUID(uuid_high, uuid_low), icon_id, caption); + if (mFwMajor >= 3) { + sendBytesAck.encodedBytes = encodeActionResponse(uuid, icon_id, caption); } else { sendBytesAck.encodedBytes = encodeActionResponse2x(id, action, 6, caption); } @@ -1817,6 +1877,17 @@ public class PebbleProtocol extends GBDeviceProtocol { return null; } + private void decodeAppLogs(ByteBuffer buf) { + UUID uuid = getUUID(buf); + int timestamp = buf.getInt(); + int logLevel = buf.get() & 0xff; + int messageLength = buf.get() & 0xff; + int lineNumber = buf.getShort() & 0xffff; + String fileName = getFixedString(buf, 16); + String message = getFixedString(buf, messageLength); + LOG.debug("APP_LOGS (" + logLevel +") from uuid " + uuid.toString() + " in " + fileName + ":" + lineNumber + " " + message); + } + private GBDeviceEvent decodeSystemMessage(ByteBuffer buf) { buf.get(); // unknown; byte command = buf.get(); @@ -1837,9 +1908,7 @@ public class PebbleProtocol extends GBDeviceProtocol { private GBDeviceEvent[] decodeAppRunState(ByteBuffer buf) { byte command = buf.get(); - long uuid_high = buf.getLong(); - long uuid_low = buf.getLong(); - UUID uuid = new UUID(uuid_high, uuid_low); + UUID uuid = getUUID(buf); final String ENDPOINT_NAME = "APPRUNSTATE"; switch (command) { case APPRUNSTATE_START: @@ -1888,9 +1957,7 @@ public class PebbleProtocol extends GBDeviceProtocol { private GBDeviceEventAppManagement decodeAppFetch(ByteBuffer buf) { byte command = buf.get(); if (command == 0x01) { - long uuid_high = buf.getLong(); - long uuid_low = buf.getLong(); - UUID uuid = new UUID(uuid_high, uuid_low); + UUID uuid = getUUID(buf); buf.order(ByteOrder.LITTLE_ENDIAN); int app_id = buf.getInt(); GBDeviceEventAppManagement fetchRequest = new GBDeviceEventAppManagement(); @@ -1923,10 +1990,7 @@ public class PebbleProtocol extends GBDeviceProtocol { } break; case DATALOG_OPENSESSION: - buf.order(ByteOrder.BIG_ENDIAN); - long uuid_high = buf.getLong(); - long uuid_low = buf.getLong(); - UUID uuid = new UUID(uuid_high, uuid_low); + UUID uuid = getUUID(buf); buf.order(ByteOrder.LITTLE_ENDIAN); int timestamp = buf.getInt(); int log_tag = buf.getInt(); @@ -1935,11 +1999,13 @@ public class PebbleProtocol extends GBDeviceProtocol { LOG.info("DATALOG OPENSESSION. id=" + (id & 0xff) + ", App UUID=" + uuid.toString() + ", log_tag=" + log_tag + ", item_type=" + item_type + ", itemSize=" + item_size); if (!mDatalogSessions.containsKey(id)) { if (uuid.equals(UUID_ZERO) && log_tag == 81) { - mDatalogSessions.put(id, new DatalogSessionHealthSteps(id, uuid, log_tag, item_type, item_size)); + mDatalogSessions.put(id, new DatalogSessionHealthSteps(id, uuid, log_tag, item_type, item_size, getDevice())); } else if (uuid.equals(UUID_ZERO) && log_tag == 83) { - mDatalogSessions.put(id, new DatalogSessionHealthSleep(id, uuid, log_tag, item_type, item_size)); + mDatalogSessions.put(id, new DatalogSessionHealthSleep(id, uuid, log_tag, item_type, item_size, getDevice())); } else if (uuid.equals(UUID_ZERO) && log_tag == 84) { - mDatalogSessions.put(id, new DatalogSessionHealthOverlayData(id, uuid, log_tag, item_type, item_size)); + mDatalogSessions.put(id, new DatalogSessionHealthOverlayData(id, uuid, log_tag, item_type, item_size, getDevice())); + } else if (uuid.equals(UUID_ZERO) && log_tag == 85) { + mDatalogSessions.put(id, new DatalogSessionHealthHR(id, uuid, log_tag, item_type, item_size, getDevice())); } else { mDatalogSessions.put(id, new DatalogSession(id, uuid, log_tag, item_type, item_size)); } @@ -1976,6 +2042,32 @@ public class PebbleProtocol extends GBDeviceProtocol { return null; } + private GBDeviceEvent decodeVoiceControl(ByteBuffer buf) { + buf.order(ByteOrder.LITTLE_ENDIAN); + byte command = buf.get(); + int flags = buf.getInt(); + byte session_type = buf.get(); //0x01 dictation 0x02 command + short session_id = buf.getShort(); + //attributes + byte count = buf.get(); + byte type = buf.get(); + short length = buf.getShort(); + byte[] version = new byte[20]; + buf.get(version); //it's a string like "1.2rc1" + int sample_rate = buf.getInt(); + short bit_rate = buf.getShort(); + byte bitstream_version = buf.get(); + short frame_size = buf.getShort(); + + GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes(); + if (command == 0x01) { //session setup + sendBytes.encodedBytes = null; + } else if (command == 0x02) { //dictation result + sendBytes.encodedBytes = null; + } + return sendBytes; + } + @Override public GBDeviceEvent[] decodeResponse(byte[] responseData) { ByteBuffer buf = ByteBuffer.wrap(responseData); @@ -1983,7 +2075,7 @@ public class PebbleProtocol extends GBDeviceProtocol { short length = buf.getShort(); short endpoint = buf.getShort(); GBDeviceEvent devEvts[] = null; - byte pebbleCmd = -1; + byte pebbleCmd; switch (endpoint) { case ENDPOINT_MUSICCONTROL: pebbleCmd = buf.get(); @@ -2033,16 +2125,14 @@ public class PebbleProtocol extends GBDeviceProtocol { GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); buf.getInt(); // skip - byte[] tmp = new byte[32]; - buf.get(tmp, 0, 32); + versionCmd.fwVersion = getFixedString(buf, 32); - versionCmd.fwVersion = new String(tmp).trim(); - if (versionCmd.fwVersion.startsWith("v3")) { - isFw3x = true; - } + mFwMajor = versionCmd.fwVersion.charAt(1) - 48; + LOG.info("Pebble firmware major detected as " + mFwMajor); + byte[] tmp = new byte[9]; buf.get(tmp, 0, 9); - int hwRev = buf.get() + 5; + int hwRev = buf.get() + 8; if (hwRev >= 0 && hwRev < hwRevisions.length) { versionCmd.hwVersion = hwRevisions[hwRev]; } @@ -2055,8 +2145,6 @@ public class PebbleProtocol extends GBDeviceProtocol { GBDeviceEventAppInfo appInfoCmd = new GBDeviceEventAppInfo(); int slotCount = buf.getInt(); int slotsUsed = buf.getInt(); - byte[] appName = new byte[32]; - byte[] appCreator = new byte[32]; appInfoCmd.apps = new GBDeviceApp[slotsUsed]; boolean[] slotInUse = new boolean[slotCount]; @@ -2064,8 +2152,9 @@ public class PebbleProtocol extends GBDeviceProtocol { int id = buf.getInt(); int index = buf.getInt(); slotInUse[index] = true; - buf.get(appName, 0, 32); - buf.get(appCreator, 0, 32); + String appName = getFixedString(buf, 32); + String appCreator = getFixedString(buf, 32); + int flags = buf.getInt(); GBDeviceApp.Type appType; @@ -2077,7 +2166,7 @@ public class PebbleProtocol extends GBDeviceProtocol { appType = GBDeviceApp.Type.APP_GENERIC; } Short appVersion = buf.getShort(); - appInfoCmd.apps[i] = new GBDeviceApp(tmpUUIDS.get(i), new String(appName).trim(), new String(appCreator).trim(), appVersion.toString(), appType); + appInfoCmd.apps[i] = new GBDeviceApp(tmpUUIDS.get(i), appName, appCreator, appVersion.toString(), appType); } for (int i = 0; i < slotCount; i++) { if (!slotInUse[i]) { @@ -2095,9 +2184,7 @@ public class PebbleProtocol extends GBDeviceProtocol { tmpUUIDS.clear(); slotsUsed = buf.getInt(); for (int i = 0; i < slotsUsed; i++) { - long uuid_high = buf.getLong(); - long uuid_low = buf.getLong(); - UUID uuid = new UUID(uuid_high, uuid_low); + UUID uuid = getUUID(buf); LOG.info("found uuid: " + uuid); tmpUUIDS.add(uuid); } @@ -2142,13 +2229,10 @@ public class PebbleProtocol extends GBDeviceProtocol { case ENDPOINT_LAUNCHER: pebbleCmd = buf.get(); last_id = buf.get(); - long uuid_high = buf.getLong(); - long uuid_low = buf.getLong(); + UUID uuid = getUUID(buf); switch (pebbleCmd) { case APPLICATIONMESSAGE_PUSH: - UUID uuid = new UUID(uuid_high, uuid_low); - if (endpoint == ENDPOINT_LAUNCHER) { LOG.info("got LAUNCHER PUSH from UUID " + uuid); break; @@ -2228,6 +2312,15 @@ public class PebbleProtocol extends GBDeviceProtocol { break; case ENDPOINT_APPREORDER: devEvts = new GBDeviceEvent[]{decodeAppReorder(buf)}; + break; + case ENDPOINT_APPLOGS: + decodeAppLogs(buf); + break; +// case ENDPOINT_VOICECONTROL: +// devEvts = new GBDeviceEvent[]{decodeVoiceControl(buf)}; +// case ENDPOINT_AUDIOSTREAM: +// LOG.debug(GB.hexdump(responseData, 0, responseData.length)); +// break; default: break; } @@ -2235,8 +2328,24 @@ public class PebbleProtocol extends GBDeviceProtocol { return devEvts; } - public void setForceProtocol(boolean force) { + void setForceProtocol(boolean force) { LOG.info("setting force protocol to " + force); mForceProtocol = force; } + + private String getFixedString(ByteBuffer buf, int length) { + byte[] tmp = new byte[length]; + buf.get(tmp, 0, length); + + return new String(tmp).trim(); + } + + private UUID getUUID(ByteBuffer buf) { + ByteOrder byteOrder = buf.order(); + buf.order(ByteOrder.BIG_ENDIAN); + long uuid_high = buf.getLong(); + long uuid_low = buf.getLong(); + buf.order(byteOrder); + return new UUID(uuid_high, uuid_low); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java index 7233a2be..2b37852a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java @@ -3,6 +3,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble; import android.net.Uri; import android.util.Pair; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -31,7 +32,7 @@ public class PebbleSupport extends AbstractSerialDeviceSupport { @Override protected GBDeviceProtocol createDeviceProtocol() { - return new PebbleProtocol(); + return new PebbleProtocol(getDevice()); } @Override @@ -59,6 +60,14 @@ public class PebbleSupport extends AbstractSerialDeviceSupport { while (keysIterator.hasNext()) { String keyStr = keysIterator.next(); Object object = json.get(keyStr); + if (object instanceof JSONArray) { + JSONArray jsonArray = (JSONArray) object; + byte[] byteArray = new byte[jsonArray.length()]; + for (int i = 0; i < jsonArray.length(); i++) { + byteArray[i] = ((Integer) jsonArray.get(i)).byteValue(); + } + object = byteArray; + } pairs.add(new Pair<>(Integer.parseInt(keyStr), object)); } getDeviceIOThread().write(((PebbleProtocol) getDeviceProtocol()).encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, uuid, pairs)); @@ -72,6 +81,11 @@ public class PebbleSupport extends AbstractSerialDeviceSupport { } + @Override + public void onSetConstantVibration(int intensity) { + + } + @Override public synchronized PebbleIoThread getDeviceIOThread() { return (PebbleIoThread) super.getDeviceIOThread(); @@ -142,4 +156,18 @@ public class PebbleSupport extends AbstractSerialDeviceSupport { super.onDeleteCalendarEvent(type, id); } } + + @Override + public void onSendConfiguration(String config) { + if (reconnect()) { + super.onSendConfiguration(config); + } + } + + @Override + public void onTestNewFunction() { + if (reconnect()) { + super.onTestNewFunction(); + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java new file mode 100644 index 00000000..e9935e18 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java @@ -0,0 +1,226 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.content.Context; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +import static android.bluetooth.BluetoothGattCharacteristic.FORMAT_UINT16; +import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE; + + +class PebbleGATTClient extends BluetoothGattCallback { + + private static final Logger LOG = LoggerFactory.getLogger(PebbleGATTClient.class); + + private static final UUID SERVICE_UUID = UUID.fromString("0000fed9-0000-1000-8000-00805f9b34fb"); + private static final UUID CONNECTIVITY_CHARACTERISTIC = UUID.fromString("00000001-328E-0FBB-C642-1AA6699BDADA"); + private static final UUID PAIRING_TRIGGER_CHARACTERISTIC = UUID.fromString("00000002-328E-0FBB-C642-1AA6699BDADA"); + private static final UUID MTU_CHARACTERISTIC = UUID.fromString("00000003-328e-0fbb-c642-1aa6699bdada"); + private static final UUID CONNECTION_PARAMETERS_CHARACTERISTIC = UUID.fromString("00000005-328E-0FBB-C642-1AA6699BDADA"); + private static final UUID CHARACTERISTIC_CONFIGURATION_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + + private final BluetoothDevice mBtDevice; + private final Context mContext; + private final PebbleLESupport mPebbleLESupport; + + private boolean oldPebble = false; + private boolean doPairing = true; + private boolean removeBond = false; + private BluetoothGatt mBluetoothGatt; + + PebbleGATTClient(PebbleLESupport pebbleLESupport, Context context, BluetoothDevice btDevice) { + mContext = context; + mBtDevice = btDevice; + mPebbleLESupport = pebbleLESupport; + connectToPebble(mBtDevice); + } + + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + return; + } + + if (characteristic.getUuid().equals(MTU_CHARACTERISTIC)) { + int newMTU = characteristic.getIntValue(FORMAT_UINT16, 0); + LOG.info("Pebble requested MTU: " + newMTU); + mPebbleLESupport.setMTU(newMTU); + } else { + LOG.info("onCharacteristicChanged()" + characteristic.getUuid().toString() + " " + GB.hexdump(characteristic.getValue(), 0, -1)); + } + } + + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + return; + } + + LOG.info("onCharacteristicRead() status = " + status); + if (status == BluetoothGatt.GATT_SUCCESS) { + LOG.info("onCharacteristicRead()" + characteristic.getUuid().toString() + " " + GB.hexdump(characteristic.getValue(), 0, -1)); + + if (oldPebble) { + subscribeToConnectivity(gatt); + } else { + subscribeToConnectionParams(gatt); + } + } + } + + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + return; + } + + LOG.info("onConnectionStateChange() status = " + status + " newState = " + newState); + if (newState == BluetoothGatt.STATE_CONNECTED) { + LOG.info("calling discoverServices()"); + gatt.discoverServices(); + } else if (newState == BluetoothGatt.STATE_DISCONNECTED) { + mPebbleLESupport.close(); + } + } + + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + return; + } + + LOG.info("onCharacteristicWrite() " + characteristic.getUuid()); + if (characteristic.getUuid().equals(PAIRING_TRIGGER_CHARACTERISTIC) || characteristic.getUuid().equals(CONNECTIVITY_CHARACTERISTIC)) { + //mBtDevice.createBond(); // did not work when last tried + + if (oldPebble) { + subscribeToConnectivity(gatt); + } else { + subscribeToConnectionParams(gatt); + } + } else if (characteristic.getUuid().equals(MTU_CHARACTERISTIC)) { + if (GBApplication.isRunningLollipopOrLater()) { + gatt.requestMtu(339); + } + } + } + + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor bluetoothGattDescriptor, int status) { + if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + return; + } + + LOG.info("onDescriptorWrite() status=" + status); + + UUID CHARACTERISTICUUID = bluetoothGattDescriptor.getCharacteristic().getUuid(); + + if (CHARACTERISTICUUID.equals(CONNECTION_PARAMETERS_CHARACTERISTIC)) { + subscribeToConnectivity(gatt); + } else if (CHARACTERISTICUUID.equals(CONNECTIVITY_CHARACTERISTIC)) { + subscribeToMTU(gatt); + } else if (CHARACTERISTICUUID.equals(MTU_CHARACTERISTIC)) { + setMTU(gatt); + } + } + + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + return; + } + + LOG.info("onServicesDiscovered() status = " + status); + if (status == BluetoothGatt.GATT_SUCCESS) { + BluetoothGattCharacteristic connectionPararmharacteristic = gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTION_PARAMETERS_CHARACTERISTIC); + oldPebble = connectionPararmharacteristic == null; + + if (oldPebble) { + LOG.info("This seems to be an older le enabled pebble"); + } + + if (doPairing) { + BluetoothGattCharacteristic characteristic = gatt.getService(SERVICE_UUID).getCharacteristic(PAIRING_TRIGGER_CHARACTERISTIC); + if ((characteristic.getProperties() & PROPERTY_WRITE) != 0) { + characteristic.setValue(new byte[]{1}); + gatt.writeCharacteristic(characteristic); + } else { + LOG.info("This seems to be some <4.0 FW Pebble, reading pairing trigger"); + gatt.readCharacteristic(characteristic); + } + } else { + if (oldPebble) { + subscribeToConnectivity(gatt); + } else { + subscribeToConnectionParams(gatt); + } + } + } + } + + private void connectToPebble(BluetoothDevice btDevice) { + if (removeBond) { + try { + Method m = btDevice.getClass() + .getMethod("removeBond", (Class[]) null); + m.invoke(btDevice, (Object[]) null); + } catch (Exception e) { + LOG.warn(e.getMessage()); + } + try { + Thread.sleep(1000); + } catch (InterruptedException ignore) { + } + } + if (mBluetoothGatt != null) { + this.close(); + } + mBluetoothGatt = btDevice.connectGatt(mContext, false, this); + } + + private void subscribeToConnectivity(BluetoothGatt gatt) { + LOG.info("subscribing to connectivity characteristic"); + BluetoothGattDescriptor descriptor = gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTIVITY_CHARACTERISTIC).getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR); + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + gatt.writeDescriptor(descriptor); + gatt.setCharacteristicNotification(gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTIVITY_CHARACTERISTIC), true); + } + + private void subscribeToMTU(BluetoothGatt gatt) { + LOG.info("subscribing to mtu characteristic"); + BluetoothGattDescriptor descriptor = gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC).getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR); + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + gatt.writeDescriptor(descriptor); + gatt.setCharacteristicNotification(gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC), true); + } + + private void subscribeToConnectionParams(BluetoothGatt gatt) { + LOG.info("subscribing to connection parameters characteristic"); + BluetoothGattDescriptor descriptor = gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTION_PARAMETERS_CHARACTERISTIC).getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR); + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + gatt.writeDescriptor(descriptor); + gatt.setCharacteristicNotification(gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTION_PARAMETERS_CHARACTERISTIC), true); + } + + private void setMTU(BluetoothGatt gatt) { + LOG.info("setting MTU"); + BluetoothGattCharacteristic characteristic = gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC); + BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR); + descriptor.setValue(new byte[]{0x0b, 0x01}); // unknown + gatt.writeCharacteristic(characteristic); + } + + public void close() { + if (mBluetoothGatt != null) { + mBluetoothGatt.disconnect(); + mBluetoothGatt.close(); + mBluetoothGatt = null; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTServer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTServer.java new file mode 100644 index 00000000..d3f54ee7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTServer.java @@ -0,0 +1,191 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattServer; +import android.bluetooth.BluetoothGattServerCallback; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; +import android.content.Context; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.UUID; + +class PebbleGATTServer extends BluetoothGattServerCallback { + private static final Logger LOG = LoggerFactory.getLogger(PebbleGATTServer.class); + private static final UUID WRITE_CHARACTERISTICS = UUID.fromString("10000001-328E-0FBB-C642-1AA6699BDADA"); + private static final UUID READ_CHARACTERISTICS = UUID.fromString("10000002-328E-0FBB-C642-1AA6699BDADA"); + private static final UUID CHARACTERISTICS_CONFIGURATION_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + private static final UUID SERVER_SERVICE = UUID.fromString("10000000-328E-0FBB-C642-1AA6699BDADA"); + private static final UUID SERVER_SERVICE_BADBAD = UUID.fromString("BADBADBA-DBAD-BADB-ADBA-BADBADBADBAD"); + private final BluetoothDevice mBtDevice; + private final PebbleLESupport mPebbleLESupport; + private Context mContext; + private BluetoothGattServer mBluetoothGattServer; + private BluetoothGattCharacteristic writeCharacteristics; + + PebbleGATTServer(PebbleLESupport pebbleLESupport, Context context, BluetoothDevice btDevice) { + mContext = context; + mBtDevice = btDevice; + mPebbleLESupport = pebbleLESupport; + } + + boolean initialize() { + BluetoothManager bluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE); + + mBluetoothGattServer = bluetoothManager.openGattServer(mContext, this); + if (mBluetoothGattServer == null) { + return false; + } + + BluetoothGattService pebbleGATTService = new BluetoothGattService(SERVER_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY); + pebbleGATTService.addCharacteristic(new BluetoothGattCharacteristic(READ_CHARACTERISTICS, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ)); + + writeCharacteristics = new BluetoothGattCharacteristic(WRITE_CHARACTERISTICS, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE | BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_WRITE); + + writeCharacteristics.addDescriptor(new BluetoothGattDescriptor(CHARACTERISTICS_CONFIGURATION_DESCRIPTOR, BluetoothGattDescriptor.PERMISSION_WRITE)); + pebbleGATTService.addCharacteristic(writeCharacteristics); + mBluetoothGattServer.addService(pebbleGATTService); + + return true; + } + + synchronized void sendDataToPebble(byte[] data) { + //LOG.info("send data to pebble " + GB.hexdump(data, 0, -1)); + writeCharacteristics.setValue(data.clone()); + + mBluetoothGattServer.notifyCharacteristicChanged(mBtDevice, writeCharacteristics, false); + } + + synchronized private void sendAckToPebble(int serial) { + writeCharacteristics.setValue(new byte[]{(byte) (((serial << 3) | 1) & 0xff)}); + + mBluetoothGattServer.notifyCharacteristicChanged(mBtDevice, writeCharacteristics, false); + + try { + Thread.sleep(100); // FIXME: bad bad, I mean BAAAD + } catch (InterruptedException ignore) { + } + } + + public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) { + if (!mPebbleLESupport.isExpectedDevice(device)) { + return; + } + + if (!characteristic.getUuid().equals(READ_CHARACTERISTICS)) { + LOG.warn("unexpected read request"); + return; + } + + LOG.info("will send response to read request from device: " + device.getAddress()); + if (!this.mBluetoothGattServer.sendResponse(device, requestId, 0, offset, new byte[]{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})) { + LOG.warn("error sending response"); + } + } + + + public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, + boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { + if (!mPebbleLESupport.isExpectedDevice(device)) { + return; + } + + if (!characteristic.getUuid().equals(WRITE_CHARACTERISTICS)) { + LOG.warn("unexpected write request"); + return; + } + if (!mPebbleLESupport.mIsConnected) { + mPebbleLESupport.mIsConnected = true; + synchronized (mPebbleLESupport) { + mPebbleLESupport.notify(); + } + } + //LOG.info("write request: offset = " + offset + " value = " + GB.hexdump(value, 0, -1)); + int header = value[0] & 0xff; + int command = header & 7; + int serial = header >> 3; + if (command == 0x01) { + LOG.info("got ACK for serial = " + serial); + } + if (command == 0x02) { // some request? + LOG.info("got command 0x02"); + if (value.length > 1) { + sendDataToPebble(new byte[]{0x03, 0x19, 0x19}); // no we don't know what that means + mPebbleLESupport.createPipedInputReader(); // FIXME: maybe not here + } else { + sendDataToPebble(new byte[]{0x03}); // no we don't know what that means + } + } else if (command == 0) { // normal package + LOG.info("got PPoGATT package serial = " + serial + " sending ACK"); + + sendAckToPebble(serial); + + mPebbleLESupport.writeToPipedOutputStream(value, 1, value.length - 1); + } + } + + public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { + if (!mPebbleLESupport.isExpectedDevice(device)) { + return; + } + + LOG.info("Connection state change for device: " + device.getAddress() + " status = " + status + " newState = " + newState); + if (newState == BluetoothGattServer.STATE_DISCONNECTED) { + mPebbleLESupport.close(); + } + } + + public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, + boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { + + if (!mPebbleLESupport.isExpectedDevice(device)) { + return; + } + + if (!descriptor.getCharacteristic().getUuid().equals(WRITE_CHARACTERISTICS)) { + LOG.warn("unexpected write request"); + return; + } + + LOG.info("onDescriptorWriteRequest() notifications enabled = " + (value[0] == 1)); + if (!this.mBluetoothGattServer.sendResponse(device, requestId, 0, offset, value)) { + LOG.warn("onDescriptorWriteRequest() error sending response!"); + } + } + + public void onServiceAdded(int status, BluetoothGattService service) { + LOG.info("onServiceAdded() status = " + status + " service = " + service.getUuid()); + if (status == BluetoothGatt.GATT_SUCCESS && service.getUuid().equals(SERVER_SERVICE)) { + final BluetoothGattService badbadService = new BluetoothGattService(SERVER_SERVICE_BADBAD, BluetoothGattService.SERVICE_TYPE_PRIMARY); + badbadService.addCharacteristic(new BluetoothGattCharacteristic(SERVER_SERVICE_BADBAD, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ)); + mBluetoothGattServer.addService(badbadService); + } + } + + @Override + public void onMtuChanged(BluetoothDevice device, int mtu) { + if (!mPebbleLESupport.isExpectedDevice(device)) { + return; + } + + LOG.info("Pebble requested mtu for server: " + mtu); + mPebbleLESupport.setMTU(mtu); + } + + public void onNotificationSent(BluetoothDevice bluetoothDevice, int status) { + //LOG.info("onNotificationSent() status = " + status + " to device " + mmBtDevice.getAddress()); + } + + void close() { + if (mBluetoothGattServer != null) { + mBluetoothGattServer.cancelConnection(mBtDevice); + mBluetoothGattServer.clearServices(); + mBluetoothGattServer.close(); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleLESupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleLESupport.java new file mode 100644 index 00000000..6fa3d2c2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleLESupport.java @@ -0,0 +1,171 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.ble; + +import android.bluetooth.BluetoothDevice; +import android.content.Context; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; + +public class PebbleLESupport { + private static final Logger LOG = LoggerFactory.getLogger(PebbleLESupport.class); + private final BluetoothDevice mBtDevice; + private PipeReader mPipeReader; + private PebbleGATTServer mPebbleGATTServer; + private PebbleGATTClient mPebbleGATTClient; + private PipedInputStream mPipedInputStream; + private PipedOutputStream mPipedOutputStream; + private int mMTU = 20; + boolean mIsConnected = false; + + public PebbleLESupport(Context context, final BluetoothDevice btDevice, PipedInputStream pipedInputStream, PipedOutputStream pipedOutputStream) throws IOException { + mBtDevice = btDevice; + mPipedInputStream = new PipedInputStream(); + mPipedOutputStream = new PipedOutputStream(); + try { + pipedOutputStream.connect(mPipedInputStream); + pipedInputStream.connect(mPipedOutputStream); + } catch (IOException e) { + LOG.warn("could not connect input stream"); + } + + mPebbleGATTServer = new PebbleGATTServer(this, context, mBtDevice); + if (mPebbleGATTServer.initialize()) { + mPebbleGATTClient = new PebbleGATTClient(this, context, mBtDevice); + try { + synchronized (this) { + wait(30000); + if (mIsConnected) { + return; + } + } + } catch (InterruptedException ignored) { + } + } + this.close(); + throw new IOException("connection failed"); + } + + void writeToPipedOutputStream(byte[] value, int offset, int count) { + try { + mPipedOutputStream.write(value, offset, count); + } catch (IOException e) { + LOG.warn("error writing to output stream"); + } + } + + synchronized public void close() { + destroyPipedInputReader(); + if (mPebbleGATTServer != null) { + mPebbleGATTServer.close(); + mPebbleGATTServer = null; + } + if (mPebbleGATTClient != null) { + mPebbleGATTClient.close(); + mPebbleGATTClient = null; + } + try { + mPipedInputStream.close(); + } catch (IOException ignore) { + } + try { + mPipedOutputStream.close(); + } catch (IOException ignore) { + } + } + + synchronized void createPipedInputReader() { + if (mPipeReader == null) { + mPipeReader = new PipeReader(); + } + if (!mPipeReader.isAlive()) { + mPipeReader.start(); + } + } + + synchronized private void destroyPipedInputReader() { + if (mPipeReader != null) { + mPipeReader.interrupt(); + try { + mPipeReader.join(); + } catch (InterruptedException e) { + LOG.error(e.getMessage()); + } + mPipeReader = null; + } + } + + void setMTU(int mtu) { + mMTU = mtu; + } + + private class PipeReader extends Thread { + int mmSequence = 0; + + @Override + public void run() { + byte[] buf = new byte[8192]; + int bytesRead; + while (true) { + try { + // this code is very similar to iothread, that is bad + // because we are the ones who prepared the buffer, there should be no + // need to do crazy stuff just to find out the PP boundaries again. + bytesRead = mPipedInputStream.read(buf, 0, 4); + while (bytesRead < 4) { + bytesRead += mPipedInputStream.read(buf, bytesRead, 4 - bytesRead); + } + + int length = (buf[0] & 0xff) << 8 | (buf[1] & 0xff); + bytesRead = mPipedInputStream.read(buf, 4, length); + + while (bytesRead < length) { + bytesRead += mPipedInputStream.read(buf, bytesRead + 4, length - bytesRead); + } + + + int payloadToSend = bytesRead + 4; + int srcPos = 0; + while (payloadToSend > 0) { + int chunkSize = (payloadToSend < (mMTU - 4)) ? payloadToSend : mMTU - 4; + byte[] outBuf = new byte[chunkSize + 1]; + outBuf[0] = (byte) ((mmSequence++ << 3) & 0xff); + System.arraycopy(buf, srcPos, outBuf, 1, chunkSize); + mPebbleGATTServer.sendDataToPebble(outBuf); + srcPos += chunkSize; + payloadToSend -= chunkSize; + } + + Thread.sleep(500); // FIXME ugly wait 0.5s after each pebble package send to the pebble (we do not wait for the GATT chunks) + } catch (IOException | InterruptedException e) { + LOG.info(e.getMessage()); + Thread.currentThread().interrupt(); + break; + } + } + LOG.info("Pipereader thread shut down"); + } + + @Override + public void interrupt() { + super.interrupt(); + try { + LOG.info("closing piped inputstream"); + mPipedInputStream.close(); + } catch (IOException ignore) { + } + } + } + + boolean isExpectedDevice(BluetoothDevice device) { + if (!device.getAddress().equals(mBtDevice.getAddress())) { + LOG.info("unhandled device: " + device.getAddress() + " , ignoring, will only talk to " + mBtDevice.getAddress()); + return false; + } + return true; + } +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vibratissimo/VibratissimoSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vibratissimo/VibratissimoSupport.java new file mode 100644 index 00000000..c4539a87 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vibratissimo/VibratissimoSupport.java @@ -0,0 +1,283 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.support.v4.content.LocalBroadcastManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +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; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; + +public class VibratissimoSupport extends AbstractBTLEDeviceSupport { + + private static final Logger LOG = LoggerFactory.getLogger(VibratissimoSupport.class); + private final DeviceInfoProfile deviceInfoProfile; + private final BatteryInfoProfile batteryInfoProfile; + private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String s = intent.getAction(); + if (s.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { + handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); + } else if (s.equals(BatteryInfoProfile.ACTION_BATTERY_INFO)) { + handleBatteryInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO)); + } + } + }; + + public VibratissimoSupport() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); + addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE); + addSupportedService(UUID.fromString("00001523-1212-efde-1523-785feabcd123")); + + deviceInfoProfile = new DeviceInfoProfile<>(this); + batteryInfoProfile = new BatteryInfoProfile<>(this); + addSupportedProfile(deviceInfoProfile); + addSupportedProfile(batteryInfoProfile); + + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(BatteryInfoProfile.ACTION_BATTERY_INFO); + intentFilter.addAction(DeviceInfoProfile.ACTION_DEVICE_INFO); + broadcastManager.registerReceiver(mReceiver, intentFilter); + } + + private void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) { + batteryCmd.level = (short) info.getPercentCharged(); + handleGBDeviceEvent(batteryCmd); + } + + @Override + public void dispose() { + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + broadcastManager.unregisterReceiver(mReceiver); + super.dispose(); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + requestDeviceInfo(builder); + setInitialized(builder); + batteryInfoProfile.requestBatteryInfo(builder); + return builder; + } + + private void requestDeviceInfo(TransactionBuilder builder) { + LOG.debug("Requesting Device Info!"); + deviceInfoProfile.requestDeviceInfo(builder); + } + + private void setInitialized(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + } + + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void pair() { + + } + + private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { + LOG.warn("Device info: " + info); + versionCmd.hwVersion = info.getHardwareRevision(); + versionCmd.fwVersion = info.getFirmwareRevision(); + handleGBDeviceEvent(versionCmd); + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + } + + @Override + public void onSetTime() { + + } + + @Override + public void onSetAlarms(ArrayList alarms) { + + } + + @Override + public void onSetCallState(CallSpec callSpec) { + + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + + } + + @Override + public void onEnableRealtimeSteps(boolean enable) { + + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onAppInfoReq() { + + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + + } + + @Override + public void onAppDelete(UUID uuid) { + + } + + @Override + public void onAppConfiguration(UUID appUuid, String config) { + + } + + @Override + public void onAppReorder(UUID[] uuids) { + + } + + @Override + public void onFetchActivityData() { + + } + + @Override + public void onReboot() { + + } + + @Override + public void onHeartRateTest() { + + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + + } + + @Override + public void onFindDevice(boolean start) { + onSetConstantVibration(start ? 0xff : 0x00); + } + + @Override + public void onSetConstantVibration(int intensity) { + getQueue().clear(); + BluetoothGattCharacteristic characteristic2 = getCharacteristic(UUID.fromString("00001526-1212-efde-1523-785feabcd123")); + BluetoothGattCharacteristic characteristic1 = getCharacteristic(UUID.fromString("00001524-1212-efde-1523-785feabcd123")); + + TransactionBuilder builder = new TransactionBuilder("vibration"); + builder.write(characteristic1, new byte[]{0x03, (byte) 0x80}); + + builder.write(characteristic2, new byte[]{(byte) intensity, 0x00}); + builder.queue(getQueue()); + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + if (super.onCharacteristicChanged(gatt, characteristic)) { + return true; + } + + UUID characteristicUUID = characteristic.getUuid(); + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + return false; + } + + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + if (super.onCharacteristicRead(gatt, characteristic, status)) { + return true; + } + UUID characteristicUUID = characteristic.getUuid(); + + LOG.info("Unhandled characteristic read: " + characteristicUUID); + return false; + } + + @Override + public void onSendConfiguration(String config) { + + } + + @Override + public void onTestNewFunction() { + + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/receivers/GBMusicControlReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/receivers/GBMusicControlReceiver.java index 459d3c15..eaaa046c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/receivers/GBMusicControlReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/receivers/GBMusicControlReceiver.java @@ -69,7 +69,9 @@ public class GBMusicControlReceiver extends BroadcastReceiver { Intent upIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null); KeyEvent upEvent = new KeyEvent(eventtime, eventtime, KeyEvent.ACTION_UP, keyCode, 0); upIntent.putExtra(Intent.EXTRA_KEY_EVENT, upEvent); - upIntent.setPackage(audioPlayer); + if (!"default".equals(audioPlayer)) { + upIntent.setPackage(audioPlayer); + } context.sendOrderedBroadcast(upIntent, null); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java index e963155e..9c8add33 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java @@ -1,8 +1,5 @@ package nodomain.freeyourgadget.gadgetbridge.service.serial; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; @@ -10,6 +7,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler; 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; @@ -29,9 +27,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; * to create the device specific message for the respective events and sends them to the device via {@link #sendToDevice(byte[])}. */ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport { - - private static final Logger LOG = LoggerFactory.getLogger(AbstractDeviceSupport.class); - protected GBDeviceProtocol gbDeviceProtocol; protected GBDeviceIoThread gbDeviceIOThread; @@ -124,6 +119,12 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport sendToDevice(bytes); } + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + byte[] bytes = gbDeviceProtocol.encodeSetCannedMessages(cannedMessagesSpec); + sendToDevice(bytes); + } + @Override public void onSetMusicState(MusicStateSpec stateSpec) { byte[] bytes = gbDeviceProtocol.encodeSetMusicState(stateSpec.state, stateSpec.position, stateSpec.playRate, stateSpec.shuffle, stateSpec.repeat); @@ -154,6 +155,12 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport sendToDevice(bytes); } + @Override + public void onAppReorder(UUID[] uuids) { + byte[] bytes = gbDeviceProtocol.encodeAppReorder(uuids); + sendToDevice(bytes); + } + @Override public void onFetchActivityData() { byte[] bytes = gbDeviceProtocol.encodeSynchronizeActivityData(); @@ -207,4 +214,16 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport byte[] bytes = gbDeviceProtocol.encodeDeleteCalendarEvent(type, id); sendToDevice(bytes); } + + @Override + public void onSendConfiguration(String config) { + byte[] bytes = gbDeviceProtocol.encodeSendConfiguration(config); + sendToDevice(bytes); + } + + @Override + public void onTestNewFunction() { + byte[] bytes = gbDeviceProtocol.encodeTestNewFunction(); + sendToDevice(bytes); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceIoThread.java index 31be6f02..a61d6a58 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceIoThread.java @@ -21,7 +21,7 @@ public abstract class GBDeviceIoThread extends Thread { return gbDevice; } - protected boolean connect(String btDeviceAddress) { + protected boolean connect() { return false; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java index 2492f277..c4fe1e73 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java @@ -3,11 +3,19 @@ package nodomain.freeyourgadget.gadgetbridge.service.serial; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; public abstract class GBDeviceProtocol { + private GBDevice mDevice; + + protected GBDeviceProtocol(GBDevice device) { + mDevice = device; + } + public byte[] encodeNotification(NotificationSpec notificationSpec) { return null; } @@ -20,6 +28,10 @@ public abstract class GBDeviceProtocol { return null; } + public byte[] encodeSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + return null; + } + public byte[] encodeSetMusicInfo(String artist, String album, String track, int duration, int trackCount, int trackNr) { return null; } @@ -48,6 +60,10 @@ public abstract class GBDeviceProtocol { return null; } + public byte[] encodeAppReorder(UUID[] uuids) { + return null; + } + public byte[] encodeSynchronizeActivityData() { return null; } @@ -78,7 +94,18 @@ public abstract class GBDeviceProtocol { return null; } + public byte[] encodeSendConfiguration(String config) { + return null; + } + + public byte[] encodeTestNewFunction() { return null; } + public GBDeviceEvent[] decodeResponse(byte[] responseData) { return null; } + + public GBDevice getDevice() { + return mDevice; + } + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/.gitignore b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/.gitignore new file mode 100644 index 00000000..8242cf9f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/.gitignore @@ -0,0 +1,2 @@ +small/ +SmallHelper.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java new file mode 100644 index 00000000..f08691e2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java @@ -0,0 +1,15 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +import android.os.ParcelUuid; +import android.os.Parcelable; + +public class AndroidUtils { + public static ParcelUuid[] toParcelUUids(Parcelable[] uuids) { + if (uuids == null) { + return null; + } + ParcelUuid[] uuids2 = new ParcelUuid[uuids.length]; + System.arraycopy(uuids, 0, uuids2, 0, uuids.length); + return uuids2; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java index ef507876..6781bcb0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java @@ -4,14 +4,20 @@ import android.text.format.DateUtils; import com.github.pfichtner.durationformatter.DurationFormatter; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.GBApplication; public class DateTimeUtils { + private static SimpleDateFormat DAY_STORAGE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + public static String formatDateTime(Date date) { return DateUtils.formatDateTime(GBApplication.getContext(), date.getTime(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); } @@ -55,4 +61,21 @@ public class DateTimeUtils { cal.setTimeInMillis(timestamp * 1000L); // make sure it's converted to long return cal.getTime(); } + + public static String dayToString(Date date) { + return DAY_STORAGE_FORMAT.format(date); + } + + public static Date dayFromString(String day) throws ParseException { + return DAY_STORAGE_FORMAT.parse(day); + } + + public static Date todayUTC() { + Calendar cal = getCalendarUTC(); + return cal.getTime(); + } + + public static Calendar getCalendarUTC() { + return GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC")); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index ee274dc7..0b0c6a8d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -8,20 +8,28 @@ import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributes; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; @@ -38,15 +46,20 @@ public class DeviceHelper { // lazily created private List coordinators; - // the current single coordinator (typically there's just one device connected - private DeviceCoordinator coordinator; - public boolean isSupported(GBDeviceCandidate candidate) { - if (coordinator != null && coordinator.supports(candidate)) { - return true; - } + public DeviceType getSupportedType(GBDeviceCandidate candidate) { for (DeviceCoordinator coordinator : getAllCoordinators()) { - if (coordinator.supports(candidate)) { + DeviceType deviceType = coordinator.getSupportedType(candidate); + if (deviceType.isSupported()) { + return deviceType; + } + } + return DeviceType.UNKNOWN; + } + + public boolean getSupportedType(GBDevice device) { + for (DeviceCoordinator coordinator : getAllCoordinators()) { + if (coordinator.supports(device)) { return true; } } @@ -63,6 +76,16 @@ public class DeviceHelper { return null; } + /** + * Returns the list of all available devices that are supported by Gadgetbridge. + * Note that no state is known about the returned devices. Even if one of those + * devices is connected, it will report the default not-connected state. + * + * Clients interested in the "live" devices being managed should use the class + * DeviceManager. + * @param context + * @return + */ public Set getAvailableDevices(Context context) { BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); @@ -72,68 +95,47 @@ public class DeviceHelper { GB.toast(context, context.getString(R.string.bluetooth_is_not_supported_), Toast.LENGTH_SHORT, GB.WARN); } else if (!btAdapter.isEnabled()) { GB.toast(context, context.getString(R.string.bluetooth_is_disabled_), Toast.LENGTH_SHORT, GB.WARN); - } else { - Set pairedDevices = btAdapter.getBondedDevices(); - DeviceHelper deviceHelper = DeviceHelper.getInstance(); - for (BluetoothDevice pairedDevice : pairedDevices) { - GBDevice device = deviceHelper.toSupportedDevice(pairedDevice); - if (device != null) { - availableDevices.add(device); - } - } + } + List dbDevices = getDatabaseDevices(); + // these come first, as they have the most information already + availableDevices.addAll(dbDevices); + if (btAdapter != null) { + List bondedDevices = getBondedDevices(btAdapter); + availableDevices.addAll(bondedDevices); + } - Prefs prefs = GBApplication.getPrefs(); - String miAddr = prefs.getString(MiBandConst.PREF_MIBAND_ADDRESS, ""); - if (miAddr.length() > 0) { - GBDevice miDevice = new GBDevice(miAddr, "MI", DeviceType.MIBAND); - if (!availableDevices.contains(miDevice)) { - availableDevices.add(miDevice); - } - } + Prefs prefs = GBApplication.getPrefs(); + String miAddr = prefs.getString(MiBandConst.PREF_MIBAND_ADDRESS, ""); + if (miAddr.length() > 0) { + GBDevice miDevice = new GBDevice(miAddr, "MI", DeviceType.MIBAND); + availableDevices.add(miDevice); + } - String pebbleEmuAddr = prefs.getString("pebble_emu_addr", ""); - String pebbleEmuPort = prefs.getString("pebble_emu_port", ""); - if (pebbleEmuAddr.length() >= 7 && pebbleEmuPort.length() > 0) { - GBDevice pebbleEmuDevice = new GBDevice(pebbleEmuAddr + ":" + pebbleEmuPort, "Pebble qemu", DeviceType.PEBBLE); - availableDevices.add(pebbleEmuDevice); - } + String pebbleEmuAddr = prefs.getString("pebble_emu_addr", ""); + String pebbleEmuPort = prefs.getString("pebble_emu_port", ""); + if (pebbleEmuAddr.length() >= 7 && pebbleEmuPort.length() > 0) { + GBDevice pebbleEmuDevice = new GBDevice(pebbleEmuAddr + ":" + pebbleEmuPort, "Pebble qemu", DeviceType.PEBBLE); + availableDevices.add(pebbleEmuDevice); } return availableDevices; } public GBDevice toSupportedDevice(BluetoothDevice device) { - GBDeviceCandidate candidate = new GBDeviceCandidate(device, GBDevice.RSSI_UNKNOWN); + GBDeviceCandidate candidate = new GBDeviceCandidate(device, GBDevice.RSSI_UNKNOWN, device.getUuids()); - String deviceName = device.getName(); - try { - Method method = device.getClass().getMethod("getAliasName"); - if (method != null) { - deviceName = (String) method.invoke(device); - } - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignore) { - LOG.info("Could not get device alias for " + deviceName); - } - - if (coordinator != null && coordinator.supports(candidate)) { - return new GBDevice(device.getAddress(), deviceName, coordinator.getDeviceType()); - } for (DeviceCoordinator coordinator : getAllCoordinators()) { if (coordinator.supports(candidate)) { - return new GBDevice(device.getAddress(), deviceName, coordinator.getDeviceType()); + return coordinator.createDevice(candidate); } } return null; } public DeviceCoordinator getCoordinator(GBDeviceCandidate device) { - if (coordinator != null && coordinator.supports(device)) { - return coordinator; - } synchronized (this) { for (DeviceCoordinator coord : getAllCoordinators()) { if (coord.supports(device)) { - coordinator = coord; - return coordinator; + return coord; } } } @@ -141,14 +143,10 @@ public class DeviceHelper { } public DeviceCoordinator getCoordinator(GBDevice device) { - if (coordinator != null && coordinator.supports(device)) { - return coordinator; - } synchronized (this) { for (DeviceCoordinator coord : getAllCoordinators()) { if (coord.supports(device)) { - coordinator = coord; - return coordinator; + return coord; } } } @@ -164,8 +162,89 @@ public class DeviceHelper { private List createCoordinators() { List result = new ArrayList<>(2); + result.add(new MiBand2Coordinator()); // Note: MiBand2 must come before MiBand because detection is hacky, atm result.add(new MiBandCoordinator()); result.add(new PebbleCoordinator()); + result.add(new VibratissimoCoordinator()); + result.add(new LiveviewCoordinator()); return result; } + + private List getDatabaseDevices() { + List result = new ArrayList<>(); + try (DBHandler lockHandler = GBApplication.acquireDB()) { + List activeDevices = DBHelper.getActiveDevices(lockHandler.getDaoSession()); + for (Device dbDevice : activeDevices) { + GBDevice gbDevice = toGBDevice(dbDevice); + if (gbDevice != null && DeviceHelper.getInstance().getSupportedType(gbDevice)) { + result.add(gbDevice); + } + } + return result; + + } catch (Exception e) { + GB.toast("Error retrieving devices from database", Toast.LENGTH_SHORT, GB.ERROR); + return Collections.emptyList(); + } + } + + /** + * Converts a known device from the database to a GBDevice. + * Note: The device might not be supported anymore, so callers should verify that. + * @param dbDevice + * @return + */ + public GBDevice toGBDevice(Device dbDevice) { + DeviceType deviceType = DeviceType.fromKey(dbDevice.getType()); + GBDevice gbDevice = new GBDevice(dbDevice.getIdentifier(), dbDevice.getName(), deviceType); + List deviceAttributesList = dbDevice.getDeviceAttributesList(); + if (deviceAttributesList.size() > 0) { + gbDevice.setModel(dbDevice.getModel()); + DeviceAttributes attrs = deviceAttributesList.get(0); + gbDevice.setFirmwareVersion(attrs.getFirmwareVersion1()); + gbDevice.setFirmwareVersion2(attrs.getFirmwareVersion2()); + gbDevice.setVolatileAddress(attrs.getVolatileIdentifier()); + } + + return gbDevice; + } + + private List getBondedDevices(BluetoothAdapter btAdapter) { + Set pairedDevices = btAdapter.getBondedDevices(); + List result = new ArrayList<>(pairedDevices.size()); + DeviceHelper deviceHelper = DeviceHelper.getInstance(); + for (BluetoothDevice pairedDevice : pairedDevices) { + if (pairedDevice.getName() != null && (pairedDevice.getName().startsWith("Pebble-LE ") || pairedDevice.getName().startsWith("Pebble Time LE "))) { + continue; // ignore LE Pebble (this is part of the main device now (volatileAddress) + } + GBDevice device = deviceHelper.toSupportedDevice(pairedDevice); + if (device != null) { + result.add(device); + } + } + return result; + } + + /** + * Attempts to removing the bonding with the given device. Returns true + * if bonding was supposedly successful and false if anything went wrong + * @param device + * @return + */ + public boolean removeBond(GBDevice device) throws GBException { + BluetoothAdapter defaultAdapter = BluetoothAdapter.getDefaultAdapter(); + if (defaultAdapter != null) { + BluetoothDevice remoteDevice = defaultAdapter.getRemoteDevice(device.getAddress()); + if (remoteDevice != null) { + try { + Method method = BluetoothDevice.class.getMethod("removeBond", (Class[]) null); + Object result = method.invoke(remoteDevice, (Object[]) null); + return Boolean.TRUE.equals(result); + } catch (Exception e) { + throw new GBException("Error removing bond to device: " + device, e); + } + } + } + return false; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java index 470ef392..3898818e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java @@ -5,7 +5,6 @@ import android.content.Context; import android.net.Uri; import android.os.Environment; import android.support.annotation.NonNull; -import android.util.Log; import java.io.BufferedInputStream; import java.io.BufferedReader; @@ -116,6 +115,7 @@ public class FileUtils { file.delete(); return true; } catch (FileNotFoundException e) { + GB.log("Cannot write to directory: " + dir.getAbsolutePath(), GB.INFO, e); return false; } } @@ -134,7 +134,19 @@ public class FileUtils { @NonNull private static List getWritableExternalFilesDirs() throws IOException { Context context = GBApplication.getContext(); - File[] dirs = context.getExternalFilesDirs(null); + File[] dirs; + try { + dirs = context.getExternalFilesDirs(null); + } catch (NullPointerException | UnsupportedOperationException ex) { + // workaround for robolectric 3.1.2 not implementinc getExternalFilesDirs() + // https://github.com/robolectric/robolectric/issues/2531 + File dir = context.getExternalFilesDir(null); + if (dir != null) { + dirs = new File[] { dir }; + } else { + throw ex; + } + } if (dirs == null) { throw new IOException("Unable to access external files dirs: null"); } @@ -144,13 +156,18 @@ public class FileUtils { } for (int i = 0; i < dirs.length; i++) { File dir = dirs[i]; - if (dir == null || (!dir.exists() && !dir.mkdirs())) { + if (dir == null) { continue; } + if (!dir.exists() && !dir.mkdirs()) { + GB.log("Unable to create directories: " + dir.getAbsolutePath(), GB.INFO, null); + continue; + } + // the first directory is also the primary external storage, i.e. the same as Environment.getExternalFilesDir() // TODO: check the mount state of *all* dirs when switching to later API level - if (!dir.canWrite() || (i == 0 && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))) { - Log.i(TAG, "ignoring non-writable external storage dir: " + dir); + if (i == 0 && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { + GB.log("ignoring unmounted external storage dir: " + dir, GB.INFO, null); continue; } result.add(dir); // add last diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java index 07a7d593..a349a59d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java @@ -128,7 +128,7 @@ public class GB { // file header headerbuf.put((byte) 'B'); headerbuf.put((byte) 'M'); - headerbuf.putInt(0); // size in bytes (unconpressed = 0) + headerbuf.putInt(0); // size in bytes (uncompressed = 0) headerbuf.putInt(0); // reserved headerbuf.putInt(FILE_HEADER_SIZE + INFO_HEADER_SIZE + screenshot.clut.length); @@ -139,7 +139,7 @@ public class GB { headerbuf.putShort((short) 1); // planes headerbuf.putShort((short) screenshot.bpp); headerbuf.putInt(0); // compression - headerbuf.putInt(0); // length of pixeldata in byte (uncompressed=0) + headerbuf.putInt(0); // length of pixeldata in bytes (uncompressed=0) headerbuf.putInt(0); // pixels per meter (x) headerbuf.putInt(0); // pixels per meter (y) headerbuf.putInt(screenshot.clut.length / 4); // number of colors in CLUT diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java index e895919d..024fb442 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java @@ -1,9 +1,17 @@ package nodomain.freeyourgadget.gadgetbridge.util; +import java.text.ParseException; +import java.util.Date; + public class GBPrefs { public static final String AUTO_RECONNECT = "general_autocreconnect"; public static boolean AUTO_RECONNECT_DEFAULT = true; + + public static final String USER_NAME = "mi_user_alias"; + public static final String USER_NAME_DEFAULT = "gadgetbridge-user"; + private static final String USER_BIRTHDAY = ""; + private final Prefs mPrefs; public GBPrefs(Prefs prefs) { @@ -13,4 +21,25 @@ public class GBPrefs { public boolean getAutoReconnect() { return mPrefs.getBoolean(AUTO_RECONNECT, AUTO_RECONNECT_DEFAULT); } + + public String getUserName() { + return mPrefs.getString(USER_NAME, USER_NAME_DEFAULT); + } + + public Date getUserBirthday() { + String date = mPrefs.getString(USER_BIRTHDAY, null); + if (date == null) { + return null; + } + try { + return DateTimeUtils.dayFromString(date); + } catch (ParseException ex) { + GB.log("Error parsing date: " + date, GB.ERROR, ex); + return null; + } + } + + public int getUserSex() { + return 0; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java index 11ec9678..f6597807 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PebbleUtils.java @@ -7,6 +7,10 @@ public class PebbleUtils { platformName = "basalt"; } else if (hwRev.startsWith("spalding")) { platformName = "chalk"; + } else if (hwRev.startsWith("silk")) { + platformName = "diorite"; + } else if (hwRev.startsWith("robert")) { + platformName = "emery"; } else { platformName = "aplite"; } @@ -20,9 +24,27 @@ public class PebbleUtils { model = "pebble_time_black"; } else if (hwRev.startsWith("spalding")) { model = "pebble_time_round_black_20mm"; + } else if (hwRev.startsWith("silk")) { + model = "pebble2_black"; + } else if (hwRev.startsWith("robert")) { + model = "pebble_time2_black"; } else { model = "pebble_black"; } return model; } + + public static int getFwMajor(String fwString) { + return fwString.charAt(1) - 48; + } + + public static boolean hasHRM(String hwRev) { + String platformName = getPlatformName(hwRev); + return "diorite".equals(platformName) || "emery".equals(platformName); + } + + public static boolean hasHealth(String hwRev) { + String platformName = getPlatformName(hwRev); + return !"aplite".equals(platformName); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java index 03aca1aa..875038ec 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java @@ -3,9 +3,6 @@ package nodomain.freeyourgadget.gadgetbridge.util; import android.content.SharedPreferences; import android.util.Log; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Set; /** diff --git a/app/src/main/res/drawable-hdpi/ic_activitytracker.png b/app/src/main/res/drawable-hdpi/ic_activitytracker.png index a25228b9..1b54cfb5 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_activitytracker.png and b/app/src/main/res/drawable-hdpi/ic_activitytracker.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_default_disabled.png b/app/src/main/res/drawable-hdpi/ic_device_default_disabled.png index 694e9cd9..af629f56 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_device_default_disabled.png and b/app/src/main/res/drawable-hdpi/ic_device_default_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_lovetoy.png b/app/src/main/res/drawable-hdpi/ic_device_lovetoy.png new file mode 100644 index 00000000..dee05896 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_lovetoy.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_lovetoy_disabled.png b/app/src/main/res/drawable-hdpi/ic_device_lovetoy_disabled.png new file mode 100644 index 00000000..c9589fa3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_lovetoy_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_miband.png b/app/src/main/res/drawable-hdpi/ic_device_miband.png index 2c8ce764..2f0329a1 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_device_miband.png and b/app/src/main/res/drawable-hdpi/ic_device_miband.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_pebble.png b/app/src/main/res/drawable-hdpi/ic_device_pebble.png index 49bbe621..c889b478 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_device_pebble.png and b/app/src/main/res/drawable-hdpi/ic_device_pebble.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_firmware.png b/app/src/main/res/drawable-hdpi/ic_firmware.png index d2c3d828..aba8fba8 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_firmware.png and b/app/src/main/res/drawable-hdpi/ic_firmware.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_information_outline_grey600_24dp.png b/app/src/main/res/drawable-hdpi/ic_information_outline_grey600_24dp.png index 28ac3bdd..c9bdc631 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_information_outline_grey600_24dp.png and b/app/src/main/res/drawable-hdpi/ic_information_outline_grey600_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_languagepack.png b/app/src/main/res/drawable-hdpi/ic_languagepack.png index 26b663cf..215ff1d0 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_languagepack.png and b/app/src/main/res/drawable-hdpi/ic_languagepack.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png index 0231f116..ca5ed91e 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_launcher.png and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_systemapp.png b/app/src/main/res/drawable-hdpi/ic_systemapp.png index 94991893..aa1f5ee0 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_systemapp.png and b/app/src/main/res/drawable-hdpi/ic_systemapp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_watchapp.png b/app/src/main/res/drawable-hdpi/ic_watchapp.png index 933fce03..46dea36e 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_watchapp.png and b/app/src/main/res/drawable-hdpi/ic_watchapp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_watchface.png b/app/src/main/res/drawable-hdpi/ic_watchface.png index 4f17c8f5..95fe8851 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_watchface.png and b/app/src/main/res/drawable-hdpi/ic_watchface.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_activitytracker.png b/app/src/main/res/drawable-mdpi/ic_activitytracker.png index 103a094e..ec01b54e 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_activitytracker.png and b/app/src/main/res/drawable-mdpi/ic_activitytracker.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_lovetoy.png b/app/src/main/res/drawable-mdpi/ic_device_lovetoy.png new file mode 100644 index 00000000..70fe4aa1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_lovetoy.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_lovetoy_disabled.png b/app/src/main/res/drawable-mdpi/ic_device_lovetoy_disabled.png new file mode 100644 index 00000000..2e4236a4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_lovetoy_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_miband.png b/app/src/main/res/drawable-mdpi/ic_device_miband.png index 3d65735c..29ac9990 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_device_miband.png and b/app/src/main/res/drawable-mdpi/ic_device_miband.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_pebble.png b/app/src/main/res/drawable-mdpi/ic_device_pebble.png index 47030c67..ca399415 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_device_pebble.png and b/app/src/main/res/drawable-mdpi/ic_device_pebble.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_firmware.png b/app/src/main/res/drawable-mdpi/ic_firmware.png index eb594725..883c22e6 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_firmware.png and b/app/src/main/res/drawable-mdpi/ic_firmware.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_information_outline_grey600_24dp.png b/app/src/main/res/drawable-mdpi/ic_information_outline_grey600_24dp.png index 213286b7..1812a0ef 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_information_outline_grey600_24dp.png and b/app/src/main/res/drawable-mdpi/ic_information_outline_grey600_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_languagepack.png b/app/src/main/res/drawable-mdpi/ic_languagepack.png index 41af56cf..9b77bc55 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_languagepack.png and b/app/src/main/res/drawable-mdpi/ic_languagepack.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png index bff828af..efbe18c4 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_launcher.png and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification.png b/app/src/main/res/drawable-mdpi/ic_notification.png index dd1914ad..b2447639 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_notification.png and b/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png b/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png index f1587ca3..316bc30f 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png and b/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_systemapp.png b/app/src/main/res/drawable-mdpi/ic_systemapp.png index e11604b4..245ef3bf 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_systemapp.png and b/app/src/main/res/drawable-mdpi/ic_systemapp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_watchapp.png b/app/src/main/res/drawable-mdpi/ic_watchapp.png index 9395aa82..be99a31f 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_watchapp.png and b/app/src/main/res/drawable-mdpi/ic_watchapp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_watchface.png b/app/src/main/res/drawable-mdpi/ic_watchface.png index 5aa285b4..8fc39312 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_watchface.png and b/app/src/main/res/drawable-mdpi/ic_watchface.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_activitytracker.png b/app/src/main/res/drawable-xhdpi/ic_activitytracker.png index 01f9fd5e..a1c5d5fc 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_activitytracker.png and b/app/src/main/res/drawable-xhdpi/ic_activitytracker.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_default_disabled.png b/app/src/main/res/drawable-xhdpi/ic_device_default_disabled.png index 208c6abe..0b51f592 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_device_default_disabled.png and b/app/src/main/res/drawable-xhdpi/ic_device_default_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_lovetoy.png b/app/src/main/res/drawable-xhdpi/ic_device_lovetoy.png new file mode 100644 index 00000000..bea0a9b4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_lovetoy.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_lovetoy_disabled.png b/app/src/main/res/drawable-xhdpi/ic_device_lovetoy_disabled.png new file mode 100644 index 00000000..abb43030 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_lovetoy_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_miband.png b/app/src/main/res/drawable-xhdpi/ic_device_miband.png index 4883eb92..d3f076c9 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_device_miband.png and b/app/src/main/res/drawable-xhdpi/ic_device_miband.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_pebble.png b/app/src/main/res/drawable-xhdpi/ic_device_pebble.png index d287d686..485fdf9a 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_device_pebble.png and b/app/src/main/res/drawable-xhdpi/ic_device_pebble.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_firmware.png b/app/src/main/res/drawable-xhdpi/ic_firmware.png index bd551ee4..2ef58211 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_firmware.png and b/app/src/main/res/drawable-xhdpi/ic_firmware.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_information_outline_grey600_24dp.png b/app/src/main/res/drawable-xhdpi/ic_information_outline_grey600_24dp.png index 34b5a962..86fc7a3a 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_information_outline_grey600_24dp.png and b/app/src/main/res/drawable-xhdpi/ic_information_outline_grey600_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_languagepack.png b/app/src/main/res/drawable-xhdpi/ic_languagepack.png index b71b18b6..9e4d7d07 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_languagepack.png and b/app/src/main/res/drawable-xhdpi/ic_languagepack.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png index 86ea5c4c..af114dff 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_launcher.png and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_systemapp.png b/app/src/main/res/drawable-xhdpi/ic_systemapp.png index e4402b58..125740d3 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_systemapp.png and b/app/src/main/res/drawable-xhdpi/ic_systemapp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_watchapp.png b/app/src/main/res/drawable-xhdpi/ic_watchapp.png index 562604f0..f34b2722 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_watchapp.png and b/app/src/main/res/drawable-xhdpi/ic_watchapp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_watchface.png b/app/src/main/res/drawable-xhdpi/ic_watchface.png index 2e79a456..43e2ebe0 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_watchface.png and b/app/src/main/res/drawable-xhdpi/ic_watchface.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_activitytracker.png b/app/src/main/res/drawable-xxhdpi/ic_activitytracker.png index 58591226..a07cf3d1 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_activitytracker.png and b/app/src/main/res/drawable-xxhdpi/ic_activitytracker.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_default_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_default_disabled.png index b4349b1b..b5758d1c 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_device_default_disabled.png and b/app/src/main/res/drawable-xxhdpi/ic_device_default_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_lovetoy.png b/app/src/main/res/drawable-xxhdpi/ic_device_lovetoy.png new file mode 100644 index 00000000..37d40503 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_lovetoy.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_lovetoy_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_lovetoy_disabled.png new file mode 100644 index 00000000..959b6279 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_lovetoy_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_miband.png b/app/src/main/res/drawable-xxhdpi/ic_device_miband.png index 5ad140c9..7962dc1e 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_device_miband.png and b/app/src/main/res/drawable-xxhdpi/ic_device_miband.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_miband_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_miband_disabled.png index 94dd2c12..0242632f 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_device_miband_disabled.png and b/app/src/main/res/drawable-xxhdpi/ic_device_miband_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_pebble.png b/app/src/main/res/drawable-xxhdpi/ic_device_pebble.png index eaf766b4..44a823bd 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_device_pebble.png and b/app/src/main/res/drawable-xxhdpi/ic_device_pebble.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_pebble_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_pebble_disabled.png index 24c8f2f5..351baa09 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_device_pebble_disabled.png and b/app/src/main/res/drawable-xxhdpi/ic_device_pebble_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_firmware.png b/app/src/main/res/drawable-xxhdpi/ic_firmware.png index c28e56ad..1f1769ed 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_firmware.png and b/app/src/main/res/drawable-xxhdpi/ic_firmware.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_information_outline_grey600_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_information_outline_grey600_24dp.png index fb2acbc6..585deae0 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_information_outline_grey600_24dp.png and b/app/src/main/res/drawable-xxhdpi/ic_information_outline_grey600_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_languagepack.png b/app/src/main/res/drawable-xxhdpi/ic_languagepack.png index a6154866..3d2ff442 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_languagepack.png and b/app/src/main/res/drawable-xxhdpi/ic_languagepack.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png index 11c05b85..d75e4928 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_launcher.png and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_systemapp.png b/app/src/main/res/drawable-xxhdpi/ic_systemapp.png index c4b0286c..0686d74c 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_systemapp.png and b/app/src/main/res/drawable-xxhdpi/ic_systemapp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_watchapp.png b/app/src/main/res/drawable-xxhdpi/ic_watchapp.png index 701b15db..dd2373a8 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_watchapp.png and b/app/src/main/res/drawable-xxhdpi/ic_watchapp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_watchface.png b/app/src/main/res/drawable-xxhdpi/ic_watchface.png index fdc086da..ef53407c 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_watchface.png and b/app/src/main/res/drawable-xxhdpi/ic_watchface.png differ diff --git a/app/src/main/res/drawable/gadgetbridge_img.png b/app/src/main/res/drawable/gadgetbridge_img.png index cdb748e0..176a54e0 100644 Binary files a/app/src/main/res/drawable/gadgetbridge_img.png and b/app/src/main/res/drawable/gadgetbridge_img.png differ diff --git a/app/src/main/res/drawable/ic_add_white.png b/app/src/main/res/drawable/ic_add_white.png index 3705a557..734b8238 100644 Binary files a/app/src/main/res/drawable/ic_add_white.png and b/app/src/main/res/drawable/ic_add_white.png differ diff --git a/app/src/main/res/layout-land/fragment_weeksteps_chart.xml b/app/src/main/res/layout-land/fragment_weeksteps_chart.xml new file mode 100644 index 00000000..ddd9355d --- /dev/null +++ b/app/src/main/res/layout-land/fragment_weeksteps_chart.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_appmanager.xml b/app/src/main/res/layout/activity_appmanager.xml index 868f628f..bdc6317c 100644 --- a/app/src/main/res/layout/activity_appmanager.xml +++ b/app/src/main/res/layout/activity_appmanager.xml @@ -1,16 +1,17 @@ - + - + - + + + + diff --git a/app/src/main/res/layout/activity_db_management.xml b/app/src/main/res/layout/activity_db_management.xml new file mode 100644 index 00000000..feb005a4 --- /dev/null +++ b/app/src/main/res/layout/activity_db_management.xml @@ -0,0 +1,117 @@ + + + + + + + + +