From e00ee75ad2c582755d8c81eebe67fa9dc0c555a8 Mon Sep 17 00:00:00 2001 From: vanous Date: Tue, 21 Sep 2021 16:37:19 +0200 Subject: [PATCH] adding FitPro bands support --- .../gadgetbridge/daogen/GBDaoGenerator.java | 20 +- README.md | 3 + .../AboutUserPreferencesActivity.java | 1 + .../DeviceSettingsPreferenceConst.java | 23 + .../DeviceSpecificSettingsFragment.java | 100 ++ .../devices/fitpro/FitProConstants.java | 250 +++ .../fitpro/FitProDeviceCoordinator.java | 185 ++ .../devices/fitpro/FitProSampleProvider.java | 100 ++ .../gadgetbridge/model/DeviceType.java | 1 + .../gadgetbridge/model/Weather.java | 93 +- .../service/DeviceSupportFactory.java | 4 + .../devices/fitpro/FitProDeviceSupport.java | 1560 +++++++++++++++++ .../gadgetbridge/util/ArrayUtils.java | 14 + .../gadgetbridge/util/DeviceHelper.java | 3 +- app/src/main/res/drawable/ic_chair.xml | 10 + .../main/res/drawable/ic_notifications.xml | 10 + app/src/main/res/values/arrays.xml | 52 + app/src/main/res/values/strings.xml | 13 + .../res/xml/devicesettings_autoheartrate.xml | 43 + .../devicesettings_donotdisturb_no_auto.xml | 2 +- .../main/res/xml/devicesettings_fitpro.xml | 12 + ...devicesettings_liftwrist_display_no_on.xml | 31 + .../main/res/xml/devicesettings_longsit.xml | 2 +- .../xml/devicesettings_longsit_extended.xml | 37 + .../res/xml/devicesettings_longsit_noshed.xml | 2 +- .../devicesettings_notifications_enable.xml | 10 + .../res/xml/devicesettings_sleep_time.xml | 32 + .../xml/devicesettings_vibrations_enable.xml | 10 + .../gadgetbridge/test/FitProTests.java | 50 + 29 files changed, 2667 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProConstants.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProDeviceCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/fitpro/FitProDeviceSupport.java create mode 100644 app/src/main/res/drawable/ic_chair.xml create mode 100644 app/src/main/res/drawable/ic_notifications.xml create mode 100644 app/src/main/res/xml/devicesettings_autoheartrate.xml create mode 100644 app/src/main/res/xml/devicesettings_fitpro.xml create mode 100644 app/src/main/res/xml/devicesettings_liftwrist_display_no_on.xml create mode 100644 app/src/main/res/xml/devicesettings_longsit_extended.xml create mode 100644 app/src/main/res/xml/devicesettings_notifications_enable.xml create mode 100644 app/src/main/res/xml/devicesettings_sleep_time.xml create mode 100644 app/src/main/res/xml/devicesettings_vibrations_enable.xml create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/FitProTests.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index c613cbcdd..872962bec 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -43,7 +43,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - Schema schema = new Schema(33, MAIN_PACKAGE + ".entities"); + Schema schema = new Schema(34, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -80,6 +80,7 @@ public class GBDaoGenerator { addSonySWR12Sample(schema, user, device); addBangleJSActivitySample(schema, user, device); addCasioGBX100Sample(schema, user, device); + addFitProActivitySample(schema, user, device); addHybridHRActivitySample(schema, user, device); addCalendarSyncState(schema, device); @@ -614,4 +615,21 @@ public class GBDaoGenerator { batteryLevel.addIntProperty("level").notNull(); return batteryLevel; } + + private static Entity addFitProActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "FitProActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractFitProActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + addHeartRateProperties(activitySample); + activitySample.addIntProperty("caloriesBurnt"); + activitySample.addIntProperty("distanceMeters"); + activitySample.addIntProperty("spo2Percent"); + activitySample.addIntProperty("pressureLowMmHg"); + activitySample.addIntProperty("pressureHighMmHg"); + activitySample.addIntProperty("activeTimeMinutes"); + return activitySample; + } + } diff --git a/README.md b/README.md index e257e88e4..2eeb76017 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ vendor's servers. - Casio - [GB-5600B/GB-6900B/GB-X6900B](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Casio-GB-5600B%2FGB-6900B%2FGB-X6900B) - [GBX-100](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Casio-GBX-100) + +- [FitPro](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/FitPro) - Fossil - [Hybrid HR](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Fossil-Hybrid-HR) [**\[!\]**](#special-pairing-procedures) - Q Hybrid @@ -114,6 +116,7 @@ Please see [FEATURES.md](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/ma * Taavi Eomäe (iTag) * Erik Bloß (TLW64) * Yukai Li (Lefun) +* Petr Vaněk (FitPro) ## Contribute diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AboutUserPreferencesActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AboutUserPreferencesActivity.java index f6547299b..a7054377b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AboutUserPreferencesActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AboutUserPreferencesActivity.java @@ -46,6 +46,7 @@ public class AboutUserPreferencesActivity extends AbstractSettingsActivity { addPreferenceHandlerFor(PREF_USER_HEIGHT_CM); addPreferenceHandlerFor(PREF_USER_WEIGHT_KG); addPreferenceHandlerFor(PREF_USER_GENDER); + addPreferenceHandlerFor(PREF_USER_STEPS_GOAL); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index b2613b1b8..8e9e557ee 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -21,6 +21,8 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_DATEFORMAT = "dateformat"; public static final String PREF_TIMEFORMAT = "timeformat"; public static final String PREF_WEARLOCATION = "wearlocation"; + public static final String PREF_VIBRATION_ENABLE = "vibration_enable"; + public static final String PREF_NOTIFICATION_ENABLE = "notification_enable"; public static final String PREF_SCREEN_ORIENTATION = "screen_orientation"; public static final String PREF_RESERVER_ALARMS_CALENDAR = "reserve_alarms_calendar"; public static final String PREF_ALLOW_HIGH_MTU = "allow_high_mtu"; @@ -43,8 +45,25 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_HYBRID_HR_SAVE_RAW_ACTIVITY_FILES = "save_raw_activity_files"; public static final String PREF_HYBRID_HR_DANGEROUS_EXTERNAL_INTENTS = "dangerous_external_intents"; + public static final String PREF_ACTIVATE_DISPLAY_ON_LIFT = "activate_display_on_lift_wrist"; + public static final String PREF_DISPLAY_ON_LIFT_START = "display_on_lift_start"; + public static final String PREF_DISPLAY_ON_LIFT_END = "display_on_lift_end"; + + public static final String PREF_SLEEP_TIME = "prefs_enable_sleep_time"; + public static final String PREF_SLEEP_TIME_START = "prefs_sleep_time_start"; + public static final String PREF_SLEEP_TIME_END = "prefs_sleep_time_end"; + public static final String PREF_LIFTWRIST_NOSHED = "activate_display_on_lift_wrist_noshed"; public static final String PREF_DISCONNECTNOTIF_NOSHED = "disconnect_notification_noshed"; + public static final String PREF_LONGSIT_START = "pref_longsit_start"; + public static final String PREF_LONGSIT_END = "pref_longsit_end"; + + public static final String PREF_AUTOHEARTRATE_SWITCH = "pref_autoheartrate_switch"; + public static final String PREF_AUTOHEARTRATE_SLEEP = "pref_autoheartrate_sleep"; + public static final String PREF_AUTOHEARTRATE_INTERVAL = "pref_autoheartrate_interval"; + public static final String PREF_AUTOHEARTRATE_START = "pref_autoheartrate_start"; + public static final String PREF_AUTOHEARTRATE_END = "pref_autoheartrate_end"; + public static final String PREF_POWER_MODE = "power_mode"; public static final String PREF_BUTTON_BP_CALIBRATE = "prefs_sensors_button_bp_calibration"; public static final String PREF_ALTITUDE_CALIBRATE = "pref_sensors_altitude"; @@ -52,6 +71,9 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_LONGSIT_SWITCH = "pref_longsit_switch"; public static final String PREF_LONGSIT_SWITCH_NOSHED = "screen_longsit_noshed"; public static final String PREF_DO_NOT_DISTURB_NOAUTO = "do_not_disturb_no_auto"; + public static final String PREF_DO_NOT_DISTURB_NOAUTO_START = "do_not_disturb_no_auto_start"; + public static final String PREF_DO_NOT_DISTURB_NOAUTO_END = "do_not_disturb_no_auto_end"; + public static final String PREF_FIND_PHONE_ENABLED = "prefs_find_phone"; public static final String PREF_AUTOLIGHT = "autolight"; public static final String PREF_AUTOREMOVE_MESSAGE = "autoremove_message"; @@ -74,4 +96,5 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_TRANSLITERATION_ENABLED = "pref_transliteration_enabled"; public static final String PREF_SOUNDS = "sounds"; + public static final String PREF_AUTH_KEY = "authkey"; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index 4f3c21654..197955fa8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -65,6 +65,8 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DATEFORMAT; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_FAKE_RING_DURATION; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_FIND_PHONE_ENABLED; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HYBRID_HR_DANGEROUS_EXTERNAL_INTENTS; @@ -79,9 +81,20 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LONGSIT_PERIOD; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LONGSIT_SWITCH; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LONGSIT_START; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LONGSIT_END; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_SWITCH; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_SLEEP; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_INTERVAL; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_START; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_END; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_OPERATING_SOUNDS; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_POWER_MODE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SCREEN_ORIENTATION; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SLEEP_TIME; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SLEEP_TIME_END; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SLEEP_TIME_START; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONYSWR12_LOW_VIBRATION; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONYSWR12_SMART_INTERVAL; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONYSWR12_STAMINA; @@ -90,6 +103,7 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TRANSLITERATION_ENABLED; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEARLOCATION; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_ENABLE; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_ACTIVATE_DISPLAY_ON_LIFT; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION; @@ -372,6 +386,8 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat { addPreferenceHandlerFor(PREF_EXPOSE_HR_THIRDPARTY); addPreferenceHandlerFor(PREF_BT_CONNECTED_ADVERTISEMENT); addPreferenceHandlerFor(PREF_WEARLOCATION); + addPreferenceHandlerFor(PREF_VIBRATION_ENABLE); + addPreferenceHandlerFor(PREF_NOTIFICATION_ENABLE); addPreferenceHandlerFor(PREF_SCREEN_ORIENTATION); addPreferenceHandlerFor(PREF_TIMEFORMAT); addPreferenceHandlerFor(PREF_BUTTON_1_FUNCTION_SHORT); @@ -391,7 +407,16 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat { addPreferenceHandlerFor(PREF_ALTITUDE_CALIBRATE); addPreferenceHandlerFor(PREF_LONGSIT_PERIOD); addPreferenceHandlerFor(PREF_LONGSIT_SWITCH); + addPreferenceHandlerFor(PREF_LONGSIT_START); + addPreferenceHandlerFor(PREF_LONGSIT_END); + addPreferenceHandlerFor(PREF_AUTOHEARTRATE_SWITCH); + addPreferenceHandlerFor(PREF_AUTOHEARTRATE_SLEEP); + addPreferenceHandlerFor(PREF_AUTOHEARTRATE_INTERVAL); + addPreferenceHandlerFor(PREF_AUTOHEARTRATE_START); + addPreferenceHandlerFor(PREF_AUTOHEARTRATE_END); addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO); + addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO_START); + addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO_END); addPreferenceHandlerFor(PREF_FIND_PHONE_ENABLED); addPreferenceHandlerFor(PREF_AUTOLIGHT); addPreferenceHandlerFor(PREF_AUTOREMOVE_MESSAGE); @@ -415,6 +440,81 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat { addPreferenceHandlerFor(PREF_SONYSWR12_LOW_VIBRATION); addPreferenceHandlerFor(PREF_SONYSWR12_SMART_INTERVAL); + String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF); + boolean sleepTimeScheduled = sleepTimeState.equals(PREF_DO_NOT_DISTURB_SCHEDULED); + + final Preference sleepTimeInfo = findPreference(PREF_SLEEP_TIME); + if (sleepTimeInfo != null) { + //sleepTimeInfo.setEnabled(!PREF_DO_NOT_DISTURB_OFF.equals(sleepTimeInfo)); + sleepTimeInfo.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_SLEEP_TIME); + } + }); + return true; + } + }); + } + + final Preference sleepTimeStart = findPreference(PREF_SLEEP_TIME_START); + if (sleepTimeStart != null) { + sleepTimeStart.setEnabled(sleepTimeScheduled); + sleepTimeStart.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_SLEEP_TIME_START); + } + }); + return true; + } + }); + } + + final Preference sleepTimeEnd = findPreference(PREF_SLEEP_TIME_END); + if (sleepTimeEnd != null) { + sleepTimeEnd.setEnabled(sleepTimeScheduled); + sleepTimeEnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_SLEEP_TIME_END); + } + }); + return true; + } + }); + } + + final Preference sleepTime = findPreference(PREF_SLEEP_TIME); + if (sleepTime != null) { + sleepTime.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + final boolean scheduled = PREF_DO_NOT_DISTURB_SCHEDULED.equals(newVal.toString()); + Objects.requireNonNull(sleepTimeStart).setEnabled(scheduled); + Objects.requireNonNull(sleepTimeEnd).setEnabled(scheduled); + if (sleepTimeInfo != null) { + //sleepTimeInfo.setEnabled(!PREF_DO_NOT_DISTURB_OFF.equals(newVal.toString())); + } + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_SLEEP_TIME); + } + }); + return true; + } + }); + } String displayOnLiftState = prefs.getString(PREF_ACTIVATE_DISPLAY_ON_LIFT, PREF_DO_NOT_DISTURB_OFF); boolean displayOnLiftScheduled = displayOnLiftState.equals(PREF_DO_NOT_DISTURB_SCHEDULED); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProConstants.java new file mode 100644 index 000000000..3787b811a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProConstants.java @@ -0,0 +1,250 @@ +/* Copyright (C) 2016-2020 Petr Vaněk + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ + +package nodomain.freeyourgadget.gadgetbridge.devices.fitpro; + +import java.util.UUID; + +public class FitProConstants { + + // cd 00 2a 05 01 0c 00 0c 0000012d000000bb000018 + // | | | | | | | | |-- payload -> + // | | | | | | | |----- payload length low + // | | | | | | |-------- payload length high + // | | | | | |----------- command + // | | | | |-------------- delimiter/version + // | | | |----------------- command group + // | | |-------------------- full length low + // | |----------------------- full length high + // |-------------------------- header + + public static final byte DATA_HEADER = (byte) 0xCD; + public static final byte DATA_HEADER_ACK = (byte) 0xDC; + + public static final byte[] DATA_TEMPLATE = { + (byte) DATA_HEADER, // header + (byte) 0x00, // delimiter or first byte of argument count? + (byte) 0, // argument count, calculated + (byte) 0, // command 1 + (byte) 0x1, // delimiter? + (byte) 0, // command 2 + (byte) 0x0, // delimiter or first byte of data length? + (byte) 0, // data length calculated + // data payload + }; + + public static final UUID UUID_CHARACTERISTIC_UART = UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9d"); + public static final UUID UUID_CHARACTERISTIC_TX = UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9d"); + public static final UUID UUID_CHARACTERISTIC_RX = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9d"); + //public static final UUID LT716_OTA = UUID.fromString("00010203-0405-0607-0809-0a0b0c0d1912"); //when in OTA mode + + public static final byte CMD_GROUP_GENERAL = (byte) 0x12; + public static final byte CMD_GROUP_BAND_INFO = (byte) 0x20; + public static final byte CMD_GROUP_RECEIVE_BUTTON_DATA = 0x1c; + public static final byte CMD_GROUP_RECEIVE_SPORTS_DATA = 0x15; + public static final byte CMD_GROUP_HEARTRATE_SETTINGS = 0x16; + public static final byte CMD_GROUP_REQUEST_DATA = 0x1a; + public static final byte CMD_GROUP_BIND = 0x14; + public static final byte CMD_GROUP_RESET = 0x1d; + + //group general 0x12 + public static final byte CMD_FIND_BAND = (byte) 0x0b; + public static final byte CMD_SET_DATE_TIME = (byte) 0x1; + public static final byte CMD_SET_LANGUAGE = (byte) 0x15; + public static final byte CMD_NOTIFICATION_MESSAGE = (byte) 0x12; + public static final byte CMD_NOTIFICATION_CALL = (byte) 0x11; + public static final byte CMD_WEATHER = (byte) 0x20; + public static final byte CMD_CAMERA = (byte) 0xc; + public static final byte CMD_HEART_RATE_MEASUREMENT = 0x18; //on/off + public static final byte CMD_DND = (byte) 0x14; + public static final byte CMD_INIT1 = 0xa; + public static final byte CMD_INIT2 = 0xc; + public static final byte CMD_INIT3 = (byte) 0xff; + + public static final byte CMD_SET_SLEEP_TIMES = (byte) 0xF; + public static final byte CMD_ALARM = (byte) 0x2; + public static final byte CMD_SET_ARM = (byte) 0x6; + public static final byte CMD_GET_HR = (byte) 0xd; //0/1 + public static final byte CMD_GET_PRESS = (byte) 0xe; //0/1 + + public static final byte CMD_NOTIFICATIONS_ENABLE = 0x7; + public static final byte[] VALUE_SET_NOTIFICATIONS_ENABLE_ON = new byte[]{0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}; + public static final byte[] VALUE_SET_NOTIFICATIONS_ENABLE_OFF = new byte[]{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}; + + public static final byte CMD_SET_LONG_SIT_REMINDER = (byte) 0x5; + public static final byte[] VALUE_SET_LONG_SIT_REMINDER_ON = new byte[]{0x0, 0x1, 0x0, (byte) 0x96}; + public static final byte[] VALUE_SET_LONG_SIT_REMINDER_OFF = new byte[]{0x0, 0x0, 0x0, (byte) 0x96, 0x4, 0x8, 0x16, 0x7f}; + + public static final byte CMD_SET_DISPLAY_ON_LIFT = (byte) 0x9; + public static final byte CMD_SET_STEP_GOAL = (byte) 0x3; + public static final byte CMD_SET_USER_DATA = (byte) 0x4; + public static final byte CMD_SET_DEVICE_VIBRATIONS = (byte) 0x8; + + //group receive sports data 0x15 + public static final byte CMD_REQUEST_DAY_STEPS_SUMMARY = 0x6; + //request steps/hr, as per + // https://github.com/vanous/MFitX/blob/AntiGoogle/app/src/main/java/anonymouls/dev/mgcex/app/backend/LM517CommandInterpreter.kt#L242 + public static final byte CMD_REQUEST_FETCH_DAY_STEPS_DATA = 0xd; + public static final byte CMD_REQUEST_STEPS_DATA1 = 0x6; + public static final byte CMD_REQUEST_STEPS_DATA0x7 = 0x7; + public static final byte CMD_REQUEST_STEPS_DATA0x8 = 0x8; + public static final byte CMD_REQUEST_STEPS_DATA0x10 = 0x10; + + //also sleep data as per mfitx + //https://github.com/vanous/MFitX/blob/AntiGoogle/app/src/main/java/anonymouls/dev/mgcex/app/backend/LM517CommandInterpreter.kt#L235 + + public static final byte RX_HEART_RATE_DATA = 0x0e; + public static final byte RX_SPORTS_MEASUREMENT = 0x18; //0/1 + public static final byte SPORTS_RECEIVE_KEY = 0x1; //0/1 + + public static final byte RX_SLEEP_DATA = 0x3; + public static final byte RX_STEP_DATA = 0x2; + // https://github.com/vanous/MFitX/blob/AntiGoogle/app/src/main/java/anonymouls/dev/mgcex/app/backend/LM517CommandInterpreter.kt#L162 + + public static final byte RX_SPORTS_DAY_DATA = 0xc; //0/1 + + //group get band info 0x20 + public static final byte CMD_RX_BAND_INFO = (byte) 0x2; + + //group request data 0x1a + public static final byte CMD_GET_STEPS_TARGET = 0x2; + public static final byte CMD_GET_HW_INFO = 0x10; + public static final byte CMD_GET_AUTO_HR = 0x8; + public static final byte CMD_GET_CONTACTS = 0xd; + + //group 0x14 + public static final byte CMD_UNBIND = (byte) 0x0; + + //group 0x1d + public static final byte CMD_RESET = (byte) 0x1; + + // group receive data + public static final byte RX_FIND_PHONE = (byte) 0x01; + public static final byte RX_CAMERA1 = (byte) 0x02; + public static final byte RX_CAMERA2 = (byte) 0x03; + public static final byte RX_CAMERA3 = (byte) 0x04; + public static final byte RX_MEDIA_PLAY_PAUSE = (byte) 0x0b; + public static final byte RX_MEDIA_FORW = (byte) 0x0c; + public static final byte RX_MEDIA_BACK = (byte) 0x0a; + + + //values + public static final byte VALUE_ON = (byte) 0x1; + public static final byte VALUE_OFF = (byte) 0x0; + + public static final byte UNIT_METRIC = (byte) 0x1; + public static final byte UNIT_IMPERIAL = (byte) 0x2; + + public static final byte GENDER_MALE = (byte) 0x1; + public static final byte GENDER_FEMALE = (byte) 0x0; + + public static final byte VALUE_SET_ARM_LEFT = (byte) 0x0; //guessing + public static final byte VALUE_SET_ARM_RIGHT = (byte) 0x1; + + + public static final byte[] VALUE_SET_DEVICE_VIBRATIONS_ENABLE = new byte[]{0x1, 0x1, 0x1, 0x1}; + public static final byte[] VALUE_SET_DEVICE_VIBRATIONS_DISABLE = new byte[]{0, 0, 0, 0}; + + public static final byte NOTIFICATION_ICON_FACEBOOK = (byte) 0x4; + public static final byte NOTIFICATION_ICON_TWITTER = (byte) 0x5; + public static final byte NOTIFICATION_ICON_WHATSAPP = (byte) 0x8; + public static final byte NOTIFICATION_ICON_LINE = (byte) 0x7; + public static final byte NOTIFICATION_ICON_SMS = (byte) 0x1; + public static final byte NOTIFICATION_ICON_WECHAT = (byte) 0x3; + public static final byte NOTIFICATION_ICON_QQ = (byte) 0x2; + public static final byte NOTIFICATION_ICON_INSTAGRAM = (byte) 0x10; + + public static final int LANG_CHINESE = 0x0; + public static final int LANG_PORTUGUESE = 0xc; + public static final int LANG_GERMAN = 0x5; + public static final int LANG_SPANISH = 0x6; + public static final int LANG_FRENCH = 0x7; + public static final int LANG_NETHERLANDS = 0xa; + public static final int LANG_POLISH = 0xb; + public static final int LANG_RUSSIAN = 0xd; + public static final int LANG_TURKISH = 0x10; + public static final int LANG_ENGLISH = 0x1; + public static final int LANG_CZECH = 0x4; + public static final int LANG_ITALIAN = 0x12; + + //00 01 00 96 03 08 16 7f + //^^ zeros ^^ on/off ^^ sep ^^unknown ^^minutes ^^from ^^ to ^^ unknown + //minutes are array of minutes by 15, in 45,60, 75...→ 3,4,5... + //the byte 4 → 96 could be experimented with to set different values..., byte 3 is probably Hi, byte 4 Low + //public static final byte[] CMD_SET_LONG_SIT_REMINDER = new byte[]{(byte) 0x12, (byte) 0x5}; + //maybe could be usign the ON/OFF? + //public static final byte[] VALUE_SET_LONG_SIT_REMINDER_ON = new byte[]{0x0, 0x1, 0x0, (byte) 0x96}; + //public static final byte[] VALUE_SET_LONG_SIT_REMINDER_OFF = new byte[]{0x0, 0x0, 0x0, (byte) 0x96, 0x4, 0x8, 0x16, 0x7f}; + + //Value: cd 00 0a 12 01 09 00 05 01 01 e0 05 28 ON + //Value: cd 00 0a 12 01 09 00 05 00 01 e0 05 28 OFF + // 00 1 2 3 4 5 6 7 8 9 10 11 12 + //byte 8 on/off + // 9,10,11,12 → time from/to + //public static final byte[] CMD_SET_DISPLAY_ON_LIFT = new byte[]{(byte) 0x12, (byte) 0x9}; + //maybe could be usign the ON/OFF? + //public static final byte VALUE_SET_DISPLAY_ON_LIFT_ON = 0x1; + //public static final byte[] VALUE_SET_DISPLAY_ON_LIFT_OFF = new byte[]{0x0, 0x0, 0x0, 0x0, 0x0}; + + //time + //0xCD 0x00 0x09 0x12 0x01 0x01 0x00 0x04 0xA5 0x83 0x73 0xDB + //find watch + //new byte[]{(byte) 0xcd, (byte) 0x00, (byte) 0x06, + // (byte) 0x12, (byte) 0x01, (byte) 0x0b, (byte) 0x00, (byte) 0x01, (byte) 0x01}; + + /*init procedure: + get pair cd 00 06 12 01 0a 00 01 02 → 18, 10 + cd 00 09 12 01 01 00 04 55 f8 36 90 + cd 00 05 1a 01 0a 00 00 + cd 00 05 1a 01 0c 00 00 + cd 00 06 12 01 15 00 01 01 + cd 00 06 12 01 ff 00 01 01 + cd 00 05 1a 01 01 00 00 + dc 00 05 1a 01 00 0c 01 + cd 00 05 1a 01 0f 00 00 + cd 00 05 1a 01 10 00 00 + dc 00 05 1a 01 00 1c 01 + something cd 00 05 20 01 02 00 00 → 32, 2 + real time step cd 00 06 15 01 06 00 01 01 → 21, 6 + */ + + //received heartrate + //0xcd 0x00 0x11 0x15 0x01 0x0e 0x00 0x0c 0x2a 0xf8 0x00 0x01 0x00 0x00 0x31 0x2c 0x62 0x75 0x54 0x47 + //0xcd 0x00 0x11 0x15 0x01 0x0e 0x00 0x0c 0x2b 0x04 0x00 0x01 0x00 0x01 0x43 0x94 0x62 0x5c 0x48 0x53 + //cd001115010e000c2b04000100014394625c4853 + + //find band + // cd 00 06 12 01 0b 00 01 01 + + //set celsius/fahrenheit? + //cd0006120121000100 + //cd0006120121000101 + + //find phone + //0xcd 0x00 0x05 0x1c 0x01 0x01 0x00 0x00 + //camera: + //0xcd 0x00 0x05 0x1c 0x01 0x03 0x00 0x00 + //0xcd 0x00 0x05 0x1c 0x01 0x02 0x00 0x00 + //0xcd 0x00 0x05 0x1c 0x01 0x04 0x00 0x00 + + // << 0xCD 0x00 0x05 0x1C 0x01 0x0A 0x00 0x00 + //pause 0xCD 0x00 0x05 0x1C 0x01 0x0B 0x00 0x00 + // >> 0xCD 0x00 0x05 0x1C 0x01 0x0C 0x00 0x00 + + //0xCD 0x00 0x05 0x1C 0x01 0x03 0x00 0x00 + +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProDeviceCoordinator.java new file mode 100644 index 000000000..498e91714 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProDeviceCoordinator.java @@ -0,0 +1,185 @@ +/* Copyright (C) 2016-2020 Petr Vaněk + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ + +package nodomain.freeyourgadget.gadgetbridge.devices.fitpro; + +import android.app.Activity; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.greenrobot.dao.query.QueryBuilder; +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.entities.FitProActivitySampleDao; +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 FitProDeviceCoordinator extends AbstractDeviceCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(FitProDeviceCoordinator.class); + + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + Long deviceId = device.getId(); + QueryBuilder qb = session.getFitProActivitySampleDao().queryBuilder(); + qb.where(FitProActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities(); + } + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + try { + BluetoothDevice device = candidate.getDevice(); + String name = device.getName(); + + if (name != null && + (name.equals("M6") || + name.equals("M4")) + ) { + return DeviceType.FITPRO; + } + + } catch (Exception ex) { + LOG.error("unable to check device support", ex); + } + + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.FITPRO; + } + + @Override + public int getBondingStyle() { + // different devices seem to work differently. + // user will unfortunately need to decide + return BONDING_STYLE_ASK; + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return true; + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return new FitProSampleProvider(device, session); + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public int getAlarmSlotCount() { + return 8; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return true; + } + + @Override + public String getManufacturer() { + return "FitPro"; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return true; + } + + @Override + public boolean supportsWeather() { + return true; + } + + @Override + public boolean supportsFindDevice() { + return true; + } + + @Override + public int[] getSupportedDeviceSpecificSettings(GBDevice device) { + return new int[]{ + R.xml.devicesettings_liftwrist_display_no_on, + R.xml.devicesettings_longsit_extended, + R.xml.devicesettings_donotdisturb_no_auto, + R.xml.devicesettings_sleep_time, + R.xml.devicesettings_wearlocation, + R.xml.devicesettings_autoheartrate, + R.xml.devicesettings_vibrations_enable, + R.xml.devicesettings_notifications_enable, + R.xml.devicesettings_fitpro, + }; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProSampleProvider.java new file mode 100644 index 000000000..0621146e6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/fitpro/FitProSampleProvider.java @@ -0,0 +1,100 @@ +/* Copyright (C) 2016-2020 Petr Vaněk + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ + +package nodomain.freeyourgadget.gadgetbridge.devices.fitpro; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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.FitProActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.FitProActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public class FitProSampleProvider extends AbstractSampleProvider { + public FitProSampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getFitProActivitySampleDao(); + } + + + // as per FitProDeviceSupport.rawActivityKindToUniqueKind + + @Override + public int normalizeType(int rawType) { + switch (rawType) { + case 1: + return ActivityKind.TYPE_ACTIVITY; + case 11: + return ActivityKind.TYPE_DEEP_SLEEP; + case 12: + return ActivityKind.TYPE_LIGHT_SLEEP; + default: + return ActivityKind.TYPE_UNKNOWN; + } + } + + @Override + public int toRawActivityKind(int activityKind) { + switch (activityKind) { + case ActivityKind.TYPE_ACTIVITY: + return 1; + case ActivityKind.TYPE_DEEP_SLEEP: + return 11; + case ActivityKind.TYPE_LIGHT_SLEEP: + return 12; + default: + return 1; + } + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity / 2000f; //samples are per 5 minutes, so this should be sufficient + } + + @Override + public FitProActivitySample createActivitySample() { + return new FitProActivitySample(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return FitProActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return FitProActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return FitProActivitySampleDao.Properties.DeviceId; + } +} \ No newline at end of file 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 df8fdcf3d..7ee8b3f4f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -91,6 +91,7 @@ public enum DeviceType { MIJIA_LYWSD02(200, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_mijia_lywsd02), LEFUN(210, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_lefun), SMAQ2OSS(220, R.drawable.ic_device_default, R.drawable.ic_device_default, R.string.devicetype_smaq2oss), + FITPRO(230, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_fitpro), ITAG(250, R.drawable.ic_device_itag, R.drawable.ic_device_itag_disabled, R.string.devicetype_itag), NUTMINI(251, R.drawable.ic_device_itag, R.drawable.ic_device_itag_disabled, R.string.devicetype_nut_mini), VIBRATISSIMO(300, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_vibratissimo), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java index 0b1eca63c..a8989b56d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java @@ -86,7 +86,7 @@ public class Weather { } private static final Weather weather = new Weather(); - public static Weather getInstance() {return weather;} + public static Weather getInstance() { return weather; } public static byte mapToPebbleCondition(int openWeatherMapCondition) { /* deducted values: @@ -880,4 +880,95 @@ public class Weather { return 5; } } + + public static byte mapToFitProCondition(int openWeatherMapCondition) { + switch (openWeatherMapCondition) { + case 100: + return 1; + case 104: + return 2; + case 101: + case 102: + case 103: + return 3; + case 305: + case 309: + return 4; + case 306: + case 314: + case 399: + return 5; + case 307: + case 308: + case 310: + case 311: + case 312: + case 315: + case 316: + case 317: + case 318: + return 6; + case 300: + case 301: + case 302: + case 303: + return 7; + case 400: + case 407: + return 8; + case 401: + case 408: + case 499: + return 9; + case 402: + case 403: + case 409: + case 410: + return 10; + case 404: + case 405: + case 406: + return 11; + case 500: + case 501: + case 502: + case 509: + case 510: + case 511: + case 512: + case 513: + case 514: + case 515: + return 12; + case 304: + case 313: + return 13; + case 503: + case 504: + case 507: + case 508: + return 14; + case 200: + case 201: + case 202: + case 203: + case 204: + return 15; + case 205: + case 206: + case 207: + case 208: + return 16; + case 209: + case 210: + case 211: + return 17; + case 212: + return 18; + case 231: + return 19; + default: + return 3; + } + } } 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 567d7efdb..1daf0e588 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.fitpro.FitProDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.CasioGB6900DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.CasioGBX100DeviceSupport; @@ -354,6 +355,9 @@ public class DeviceSupportFactory { case DOMYOS_T540: deviceSupport = new ServiceDeviceSupport(new DomyosT540Support(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case FITPRO: + deviceSupport = new ServiceDeviceSupport(new FitProDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; } if (deviceSupport != null) { deviceSupport.setContext(gbDevice, mBtAdapter, mContext); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/fitpro/FitProDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/fitpro/FitProDeviceSupport.java new file mode 100644 index 000000000..0714a14f3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/fitpro/FitProDeviceSupport.java @@ -0,0 +1,1560 @@ +/* Copyright (C) 2016-2020 Petr Vaněk + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ + +package nodomain.freeyourgadget.gadgetbridge.service.devices.fitpro; + +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_ALARM; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_DND; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_FIND_BAND; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GET_HW_INFO; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_BAND_INFO; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_BIND; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_GENERAL; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_HEARTRATE_SETTINGS; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_RECEIVE_BUTTON_DATA; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_RECEIVE_SPORTS_DATA; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_REQUEST_DATA; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_RESET; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_HEART_RATE_MEASUREMENT; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_NOTIFICATIONS_ENABLE; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_NOTIFICATION_CALL; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_NOTIFICATION_MESSAGE; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_REQUEST_STEPS_DATA0x10; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_REQUEST_STEPS_DATA0x7; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_REQUEST_STEPS_DATA0x8; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_REQUEST_STEPS_DATA1; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_RESET; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_RX_BAND_INFO; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_ARM; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_DATE_TIME; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_DEVICE_VIBRATIONS; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_DISPLAY_ON_LIFT; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_LANGUAGE; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_LONG_SIT_REMINDER; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_SLEEP_TIMES; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_STEP_GOAL; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_USER_DATA; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_UNBIND; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_WEATHER; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.GENDER_FEMALE; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.GENDER_MALE; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_FACEBOOK; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_INSTAGRAM; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_LINE; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_QQ; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_SMS; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_TWITTER; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_WECHAT; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_WHATSAPP; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_CAMERA1; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_CAMERA2; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_CAMERA3; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_FIND_PHONE; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_HEART_RATE_DATA; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_MEDIA_BACK; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_MEDIA_FORW; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_MEDIA_PLAY_PAUSE; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_SLEEP_DATA; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_SPORTS_DAY_DATA; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_STEP_DATA; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.UNIT_IMPERIAL; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.UNIT_METRIC; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.UUID_CHARACTERISTIC_RX; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.UUID_CHARACTERISTIC_TX; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_OFF; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_ON; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_ARM_LEFT; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_ARM_RIGHT; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_DEVICE_VIBRATIONS_DISABLE; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_DEVICE_VIBRATIONS_ENABLE; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_LONG_SIT_REMINDER_OFF; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_LONG_SIT_REMINDER_ON; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_NOTIFICATIONS_ENABLE_OFF; +import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_NOTIFICATIONS_ENABLE_ON; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; +import android.net.Uri; +import android.widget.Toast; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.FitProActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +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.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.model.Weather; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +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.SetDeviceBusyAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class FitProDeviceSupport extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(FitProDeviceSupport.class); + public final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + public final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + public final DeviceInfoProfile deviceInfoProfile; + public final BatteryInfoProfile batteryInfoProfile; + + public BluetoothGattCharacteristic readCharacteristic; + public BluetoothGattCharacteristic writeCharacteristic; + private static final boolean debugEnabled = false; + + public FitProDeviceSupport() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); + addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE); + + IntentListener mListener = new IntentListener() { + @Override + public void notify(Intent intent) { + String action = intent.getAction(); + if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) { + handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); + } else if (BatteryInfoProfile.ACTION_BATTERY_INFO.equals(action)) { + handleBatteryInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO)); + } + } + }; + + deviceInfoProfile = new DeviceInfoProfile<>(this); + deviceInfoProfile.addListener(mListener); + addSupportedProfile(deviceInfoProfile); + + batteryInfoProfile = new BatteryInfoProfile<>(this); + batteryInfoProfile.addListener(mListener); + addSupportedProfile(batteryInfoProfile); + addSupportedService(FitProConstants.UUID_CHARACTERISTIC_RX); + addSupportedService(FitProConstants.UUID_CHARACTERISTIC_UART); + } + + @Override + public TransactionBuilder initializeDevice(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + readCharacteristic = getCharacteristic(UUID_CHARACTERISTIC_RX); + writeCharacteristic = getCharacteristic(UUID_CHARACTERISTIC_TX); + + builder.notify(getCharacteristic(UUID_CHARACTERISTIC_RX), true); + builder.notify(getCharacteristic(GattService.UUID_SERVICE_BATTERY_SERVICE), true); + builder.setGattCallback(this); + + deviceInfoProfile.requestDeviceInfo(builder); + batteryInfoProfile.requestBatteryInfo(builder); + batteryInfoProfile.enableNotify(builder, true); + deviceInfoProfile.enableNotify(builder, true); + + // this sequence seems to be important as without it: + // - fetch steps doesn't work + // - band seems to drain battery really fast + // - the wait time is needed as the band must process each command + // - (implementation based on individual requests did not work, the wait is still needed) + + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, FitProConstants.CMD_INIT1, (byte) 0x2)); + setTime(builder); + builder.wait(200); + builder.write(writeCharacteristic, craftData(CMD_GROUP_REQUEST_DATA, FitProConstants.CMD_INIT1)); + builder.wait(200); + builder.write(writeCharacteristic, craftData(CMD_GROUP_REQUEST_DATA, FitProConstants.CMD_INIT2)); + builder.wait(200); + setLanguage(builder); + builder.wait(200); + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, FitProConstants.CMD_INIT3, VALUE_ON)); + builder.wait(200); + builder.write(writeCharacteristic, craftData(CMD_GROUP_REQUEST_DATA, VALUE_ON)); + builder.wait(200); + builder.write(writeCharacteristic, craftData(CMD_GROUP_REQUEST_DATA, (byte) 0xf)); + builder.wait(200); + builder.write(writeCharacteristic, craftData(CMD_GROUP_REQUEST_DATA, CMD_GET_HW_INFO)); + builder.wait(200); + builder.write(writeCharacteristic, craftData(CMD_GROUP_BAND_INFO, CMD_RX_BAND_INFO)); + builder.wait(200); + + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + return builder; + } + + public void handleDeviceInfo(DeviceInfo info) { + LOG.debug("fitpro device info: " + info); + versionCmd.hwVersion = "FitPro"; + versionCmd.fwVersion = info.getFirmwareRevision(); + handleGBDeviceEvent(versionCmd); + } + + public void handleDeviceInfo(byte[] value) { + LOG.debug("FitPro device info2"); + //test this 0xCD 0x00 0x11 0x15 0x01 0x02 0x00 0x0C 0x2B 0x27 0x00 0x01 0x33 0xA5 0x02 0x79 0x0A 0x68 0x56 0x06 + debugPrintArray(value, "Device info:"); + if (value.length < 20) { + return; + } + int start = 14; + int data_len = (int) value[start]; + + byte[] name = new byte[data_len]; + System.arraycopy(value, start + 1, name, 0, data_len); + String sName = new String(name, StandardCharsets.UTF_8); //unused for now + + start = start + data_len + 1; + data_len = (int) value[start]; + byte[] hwname = new byte[data_len]; + System.arraycopy(value, start + 1, hwname, 0, data_len); + String sHWName = new String(hwname, StandardCharsets.UTF_8); + LOG.debug("Device info: " + versionCmd); + versionCmd.hwVersion = sHWName; + handleGBDeviceEvent(versionCmd); + } + + public byte[] craftData(byte command_group, byte command, byte[] data) { + //0xCD 0x00 0x09 0x12 0x01 0x01 0x00 0x04 0xA5 0x83 0x73 0xDB + byte[] result = new byte[FitProConstants.DATA_TEMPLATE.length + data.length]; + System.arraycopy(FitProConstants.DATA_TEMPLATE, 0, result, 0, FitProConstants.DATA_TEMPLATE.length); + result[2] = (byte) (FitProConstants.DATA_TEMPLATE.length + data.length - 3); + result[3] = command_group; + result[5] = command; + result[7] = (byte) data.length; + System.arraycopy(data, 0, result, 8, data.length); + //debug + debugPrintArray(result, "crafted packet"); + return result; + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + super.onCharacteristicChanged(gatt, characteristic); + UUID characteristicUUID = characteristic.getUuid(); + byte[] data = characteristic.getValue(); + debugPrintArray(data, "FitPro received value"); + if (data[0] != FitProConstants.DATA_HEADER) { + if (debugEnabled) { + LOG.info("FitPro, packet not starting with 0xcd: " + data[0]); + debugPrintArray(new byte[]{data[0]}, "first byte"); + LOG.info("Characteristic changed UUID: " + characteristicUUID); + LOG.info("Characteristic changed service: " + characteristic.getService().getCharacteristics()); + debugPrintArray(data, "value bytes"); + } + indicateFinishedFetchingOperation(); + return false; + } + + if (data != null && data.length > 5) { + byte command = data[3]; + byte param = data[5]; + + switch (command) { + case CMD_GROUP_RECEIVE_BUTTON_DATA: + switch (param) { + case RX_FIND_PHONE: + handleFindPhone(); + break; + case RX_MEDIA_BACK: + case RX_MEDIA_FORW: + case RX_MEDIA_PLAY_PAUSE: + handleMediaButton(param); + break; + case RX_CAMERA1: + case RX_CAMERA2: + case RX_CAMERA3: + handleCamera(param); + break; + default: + } + break; + case CMD_GROUP_RECEIVE_SPORTS_DATA: + switch (param) { + case RX_HEART_RATE_DATA: + handleHR(data); + break; + case RX_SPORTS_DAY_DATA: + indicateStartingFetchingOperation(); + handleDayTotalsData(data); + indicateFinishedFetchingOperation(); + break; + case RX_SLEEP_DATA: + indicateStartingFetchingOperation(); + handleSleepData(data); + indicateFinishedFetchingOperation(); + break; + case RX_STEP_DATA: + indicateStartingFetchingOperation(); + handleStepData(data); + indicateFinishedFetchingOperation(); + break; + case CMD_REQUEST_STEPS_DATA0x7: + case CMD_REQUEST_STEPS_DATA0x8: + case CMD_REQUEST_STEPS_DATA0x10: + //acking this makes the band to send data + sendAck(data[3], data[1], data[2], data[5]); + break; + } + break; + case CMD_GROUP_BAND_INFO: + switch (param) { + case CMD_RX_BAND_INFO: + handleDeviceInfo(data); + break; + } + sendAck(data[3], data[1], data[2], data[5]); + break; + case CMD_GROUP_REQUEST_DATA: + switch (param) { + case CMD_GET_HW_INFO: + handleHardwareDetails(data); + break; + } + sendAck(data[3], data[1], data[2], data[5]); + break; + } + + LOG.info("Characteristic changed UUID: " + characteristicUUID); + LOG.info("Characteristic changed service: " + characteristic.getService().getCharacteristics()); + debugPrintArray(data, "value bytes"); + } + return false; + } + + public void indicateFinishedFetchingOperation() { + //LOG.debug("download finish announced"); + GB.updateTransferNotification(null, "", false, 100, getContext()); + GB.signalActivityDataFinish(); + unsetBusy(); + } + + public void indicateStartingFetchingOperation() { + GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 10, getContext()); + } + + protected void unsetBusy() { + if (getDevice().isBusy()) { + getDevice().unsetBusyTask(); + getDevice().sendDeviceUpdateIntent(getContext()); + } + } + + public void handleHardwareDetails(byte[] value) { + LOG.debug("FitPro hardware details"); + debugPrintArray(value, "Device info:"); + if (value.length < 20) { + return; + } + int start = 8; + int data_len = (int) value[start]; + + byte[] led = new byte[data_len]; + System.arraycopy(value, start + 1, led, 0, data_len); + String sLED = new String(led, StandardCharsets.UTF_8); + + start = start + data_len + 1; + data_len = (int) value[start]; + byte[] gsensor = new byte[data_len]; + System.arraycopy(value, start + 1, gsensor, 0, data_len); + String sGsensor = new String(gsensor, StandardCharsets.UTF_8); + + gbDevice.setFirmwareVersion2(sGsensor + " " + sLED); + + //the band does not like to answer when asked together for both hw info, so ask now, + // after data is already received + + TransactionBuilder builder = new TransactionBuilder("notification"); + builder.write(writeCharacteristic, craftData(CMD_GROUP_BAND_INFO, CMD_RX_BAND_INFO)); + builder.queue(getQueue()); + + } + + public void handleHR(byte[] value) { + LOG.debug("FitPro handle heart rate measurement"); + debugPrintArray(value, "value"); + if (value.length < 17) { + LOG.debug("FitPro heartrate measurement payload too short"); + return; + } + + int heartRate = (int) value[19]; + int pressureLow = (int) value[18]; + int pressureHigh = (int) value[17]; + int spo2 = (int) value[13]; + int seconds = ByteBuffer.wrap(value, 12, 4).getInt(); + sendAck(value[3], value[1], value[2], value[5]); + + if (!(heartRate > 0)) { + return; + } + handleHR(seconds, heartRate, pressureLow, pressureHigh, spo2); + } + + @Override + public void onSetCallState(CallSpec callSpec) { + LOG.debug("FitPro send call notification"); + TransactionBuilder builder = new TransactionBuilder("CALL"); + + if (callSpec.command == CallSpec.CALL_INCOMING) { + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + outputStream.write(0x1); + outputStream.write(0x0); + outputStream.write(0x0); + + if (callSpec.name != null) { + outputStream.write(callSpec.name.getBytes(StandardCharsets.UTF_8)); + outputStream.write(0x20); + } + if (callSpec.number != null) { + outputStream.write(callSpec.number.getBytes(StandardCharsets.UTF_8)); + outputStream.write(0x20); + } + + } catch (IOException e) { + LOG.error("error sending call notification: " + e); + } + debugPrintArray(craftData(CMD_GROUP_GENERAL, CMD_NOTIFICATION_CALL, outputStream.toByteArray()), "crafted call notify"); + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_NOTIFICATION_CALL, outputStream.toByteArray())); + } else { + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_NOTIFICATION_CALL, VALUE_OFF)); + } + builder.queue(getQueue()); + } + + @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, Integer id) { + + } + + @Override + public void onAppReorder(UUID[] uuids) { + + } + + @Override + public void onSendConfiguration(String config) { + + LOG.debug("FitPro on send config: " + config); + try { + TransactionBuilder builder = performInitialized("sendConfiguration"); + switch (config) { + case DeviceSettingsPreferenceConst.PREF_LANGUAGE: + setLanguage(builder); + break; + case DeviceSettingsPreferenceConst.PREF_LONGSIT_PERIOD: + case DeviceSettingsPreferenceConst.PREF_LONGSIT_SWITCH: + case DeviceSettingsPreferenceConst.PREF_LONGSIT_START: + case DeviceSettingsPreferenceConst.PREF_LONGSIT_END: + setLongSitReminder(builder); + break; + case DeviceSettingsPreferenceConst.PREF_ACTIVATE_DISPLAY_ON_LIFT: + case DeviceSettingsPreferenceConst.PREF_DISPLAY_ON_LIFT_START: + case DeviceSettingsPreferenceConst.PREF_DISPLAY_ON_LIFT_END: + setDisplayOnLift(builder); + break; + case SettingsActivity.PREF_MEASUREMENT_SYSTEM: + case ActivityUser.PREF_USER_WEIGHT_KG: + case ActivityUser.PREF_USER_GENDER: + case ActivityUser.PREF_USER_HEIGHT_CM: + case ActivityUser.PREF_USER_YEAR_OF_BIRTH: + setUserData(builder); + break; + case ActivityUser.PREF_USER_STEPS_GOAL: + setStepsGoal(builder); + break; + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START: + case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END: + setDoNotDisturb(builder); + break; + case DeviceSettingsPreferenceConst.PREF_SLEEP_TIME: + case DeviceSettingsPreferenceConst.PREF_SLEEP_TIME_START: + case DeviceSettingsPreferenceConst.PREF_SLEEP_TIME_END: + setSleepTime(builder); + break; + case DeviceSettingsPreferenceConst.PREF_WEARLOCATION: + setWearLocation(builder); + break; + case DeviceSettingsPreferenceConst.PREF_VIBRATION_ENABLE: + setVibrations(builder); + break; + case DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE: + setNotifications(builder); + break; + case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_SWITCH: + case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_SLEEP: + case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_INTERVAL: + case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_START: + case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_END: + setAutoHeartRate(builder); + break; + } + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error sending configuration: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + @Override + public void onReadConfiguration(String config) { + + } + + public void sendAck(byte command_group, byte length_high, byte length_low, byte command) { + LOG.debug(" ACKing data: " + nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils.arrayToString(new byte[]{command_group}) + " " + nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils.arrayToString(new byte[]{command})); + TransactionBuilder builder = new TransactionBuilder("notification"); + short size = (short) (ByteBuffer.wrap(new byte[]{length_high, length_low}).getShort() + 3); + byte[] sizeArray = ByteBuffer.allocate(2).putShort(size).array(); + builder.write(writeCharacteristic, new byte[]{FitProConstants.DATA_HEADER_ACK, 0, 5, command_group, 1, sizeArray[0], sizeArray[1], 1}); + builder.queue(getQueue()); + } + + @Override + public void onTestNewFunction() { + LOG.debug("Hello FitPro Test function"); + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + LOG.debug("FitPro send weather"); + short todayMax = (short) (weatherSpec.todayMaxTemp - 273); + short todayMin = (short) (weatherSpec.todayMinTemp - 273); + byte weatherUnit = 0; + String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric)); + if (units.equals(GBApplication.getContext().getString(R.string.p_unit_imperial))) { + todayMax = (short) (todayMax * 1.8f + 32); + todayMin = (short) (todayMin * 1.8f + 32); + weatherUnit = 1; + } + + byte currentConditionCode = Weather.mapToFitProCondition(weatherSpec.currentConditionCode); + TransactionBuilder builder = new TransactionBuilder("weather"); + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_WEATHER, new byte[]{(byte) todayMin, (byte) todayMax, (byte) currentConditionCode, (byte) weatherUnit})); + builder.queue(getQueue()); + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + LOG.debug("FitPro notification: " + notificationSpec.type); + TransactionBuilder builder = new TransactionBuilder("notification"); + byte icon = NOTIFICATION_ICON_SMS; + switch (notificationSpec.type) { + case GENERIC_SMS: + icon = NOTIFICATION_ICON_SMS; + break; + case FACEBOOK: + case FACEBOOK_MESSENGER: + icon = NOTIFICATION_ICON_FACEBOOK; + break; + case LINE: + icon = NOTIFICATION_ICON_LINE; + break; + case WHATSAPP: + icon = NOTIFICATION_ICON_WHATSAPP; + break; + case TWITTER: + icon = NOTIFICATION_ICON_TWITTER; + break; + case SIGNAL: + case VIBER: + case CONVERSATIONS: + icon = NOTIFICATION_ICON_QQ; + break; + case WECHAT: + case GMAIL: + icon = NOTIFICATION_ICON_WECHAT; + break; + case INSTAGRAM: + icon = NOTIFICATION_ICON_INSTAGRAM; + break; + default: + icon = NOTIFICATION_ICON_SMS; + break; + } + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + outputStream.write(icon); + outputStream.write(0x0); + outputStream.write(0x0); + + if (notificationSpec.sender != null) { + outputStream.write(notificationSpec.sender.getBytes(StandardCharsets.UTF_8)); + outputStream.write(0x20); + } else { + if (notificationSpec.phoneNumber != null) { //use number only if there is no sender + outputStream.write(notificationSpec.phoneNumber.getBytes(StandardCharsets.UTF_8)); + outputStream.write(0x20); + } + } + + if (notificationSpec.subject != null) { + outputStream.write(notificationSpec.subject.getBytes(StandardCharsets.UTF_8)); + outputStream.write(0x20); + } + if (notificationSpec.body != null) { + outputStream.write(notificationSpec.body.getBytes(StandardCharsets.UTF_8)); + outputStream.write(0x20); + } + + } catch (IOException e) { + LOG.error("FitPro error sending notification: " + e); + } + String output = outputStream.toString(); + if (outputStream.toString().length() > 60) { + output = outputStream.toString().substring(0, 60); + } + + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_NOTIFICATION_MESSAGE, output.getBytes(StandardCharsets.UTF_8))); + builder.queue(getQueue()); + } + + @Override + public void onDeleteNotification(int id) { + + } + + public FitProDeviceSupport setLanguage(TransactionBuilder builder) { + LOG.debug("FitPro set language"); + String localeString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("language", "auto"); + if (localeString == null || localeString.equals("auto")) { + String language = Locale.getDefault().getLanguage(); + String country = Locale.getDefault().getCountry(); + + if (country == null) { + country = language; + } + localeString = language + "_" + country.toUpperCase(); + } + LOG.info("Setting device to locale: " + localeString); + + byte languageCode = FitProConstants.LANG_ENGLISH; + + switch (localeString.substring(0, 2)) { + case "zh": + languageCode = FitProConstants.LANG_CHINESE; + break; + case "it": + languageCode = FitProConstants.LANG_ITALIAN; + break; + case "cs": + languageCode = FitProConstants.LANG_CZECH; + break; + case "en": + languageCode = FitProConstants.LANG_ENGLISH; + break; + case "tr": + languageCode = FitProConstants.LANG_TURKISH; + break; + case "ru": + languageCode = FitProConstants.LANG_RUSSIAN; + break; + case "pl": + languageCode = FitProConstants.LANG_POLISH; + break; + case "nl": + languageCode = FitProConstants.LANG_NETHERLANDS; + break; + case "fr": + languageCode = FitProConstants.LANG_FRENCH; + break; + case "es": + languageCode = FitProConstants.LANG_SPANISH; + break; + case "de": + languageCode = FitProConstants.LANG_GERMAN; + break; + case "pt": + languageCode = FitProConstants.LANG_PORTUGUESE; + break; + + default: + languageCode = FitProConstants.LANG_ENGLISH; + } + + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_LANGUAGE, languageCode)); + + return this; + } + + public FitProDeviceSupport setUserData(TransactionBuilder builder) { + //0xcd 0x00 0x09 0x12 0x01 0x04 0x00 0x04 0xaf 0x59 0x09 0xe1 FitPro + LOG.debug("FitPro set user data"); + + ActivityUser activityUser = new ActivityUser(); + + int age = activityUser.getAge(); + + int gender = activityUser.getGender(); + byte genderUnit = GENDER_FEMALE; + if (gender == ActivityUser.GENDER_MALE) { + genderUnit = GENDER_MALE; + } + + int heightCm = activityUser.getHeightCm(); + int weightKg = activityUser.getWeightKg(); + + byte distanceUnit = UNIT_METRIC; + String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric)); + if (units.equals(GBApplication.getContext().getString(R.string.p_unit_imperial))) { + distanceUnit = UNIT_IMPERIAL; + } + + int userData = genderUnit << 31 | age << 24 | heightCm << 15 | weightKg << 5 | distanceUnit; + byte[] data = craftData(CMD_GROUP_GENERAL, CMD_SET_USER_DATA, ByteBuffer.allocate(4).putInt(userData).array()); + builder.write(writeCharacteristic, data); + return this; + } + + @Override + public void onFetchRecordedData(int dataTypes) { + indicateFinishedFetchingOperation(); + TransactionBuilder builder = new TransactionBuilder("fetch data1"); + builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext())); + builder.write(writeCharacteristic, craftData(CMD_GROUP_RECEIVE_SPORTS_DATA, CMD_REQUEST_STEPS_DATA1, VALUE_ON)); + builder.queue(getQueue()); + } + + + public void handleDayTotalsData(byte[] value) { + LOG.debug("FitPro handle day data length: " + value.length); + debugPrintArray(value, "value"); + if (value.length < 10) { + LOG.debug("FitPro payload too short"); + return; + } + debugPrintArray(value, "processing"); + int steps = ByteBuffer.wrap(value, 10, 4).getInt(); + int distance = ByteBuffer.wrap(value, 14, 4).getInt(); + + byte[] caloriesBytes = new byte[3]; + System.arraycopy(value, 18, caloriesBytes, 0, 2); + int calories = ByteBuffer.wrap(caloriesBytes, 0, 3).getShort(); + + LOG.debug("processing day data summary, steps: " + steps + " distance: " + distance + " calories: " + calories); + sendAck(value[3], value[1], value[2], value[5]); + //handleDayTotalsData(steps, distance, calories); + } + + public void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) { + LOG.debug("FitPro battery info: " + info); + batteryCmd.level = (short) info.getPercentCharged(); + handleGBDeviceEvent(batteryCmd); + } + + /* public void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { + LOG.debug("FitPro device info: " + info); + versionCmd.hwVersion = "+FitPro"; + versionCmd.fwVersion = info.getFirmwareRevision(); + handleGBDeviceEvent(versionCmd); + } + + */ + public void handleCamera(byte command) { + GB.toast(getContext(), "Camera buttons are detected but not further handled.", Toast.LENGTH_SHORT, GB.INFO); + } + + public void handleFindPhone() { + LOG.info("FitPro find phone"); + GBDeviceEventFindPhone deviceEventFindPhone = new GBDeviceEventFindPhone(); + deviceEventFindPhone.event = GBDeviceEventFindPhone.Event.START; + evaluateGBDeviceEvent(deviceEventFindPhone); + } + + public void handleMediaButton(byte command) { + GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl(); + if (command == RX_MEDIA_PLAY_PAUSE) { + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PLAYPAUSE; + evaluateGBDeviceEvent(deviceEventMusicControl); + } else if (command == RX_MEDIA_FORW) { + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.NEXT; + evaluateGBDeviceEvent(deviceEventMusicControl); + } else if (command == RX_MEDIA_BACK) { + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PREVIOUS; + evaluateGBDeviceEvent(deviceEventMusicControl); + } + } + + public FitProDeviceSupport setVibrations(TransactionBuilder builder) { + LOG.debug("FitPro set enable vibrations"); + boolean vibrations = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_VIBRATION_ENABLE, false); + byte[] enable = VALUE_SET_DEVICE_VIBRATIONS_ENABLE; + if (!vibrations) { + enable = VALUE_SET_DEVICE_VIBRATIONS_DISABLE; + } + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_DEVICE_VIBRATIONS, enable)); + return this; + } + + public void debugPrintArray(byte[] bytes, String label) { + if (!debugEnabled) return; + String arrayString = nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils.arrayToString(bytes); + LOG.debug("FitPro debug print " + label + ": " + arrayString); + } + + public FitProDeviceSupport setNotifications(TransactionBuilder builder) { + LOG.debug("FitPro set enable notifications"); + boolean notifications = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false); + byte[] enable = VALUE_SET_NOTIFICATIONS_ENABLE_ON; + if (!notifications) { + enable = VALUE_SET_NOTIFICATIONS_ENABLE_OFF; + } + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_NOTIFICATIONS_ENABLE, enable)); + return this; + } + + public byte[] craftData(byte command_group, byte command, byte value) { + return craftData(command_group, command, new byte[]{value}); + } + + public byte[] craftData(byte command_group, byte command) { + return craftData(command_group, command, new byte[]{}); + } + + @Override + public void onSetTime() { + LOG.debug("FitPro set date and time"); + TransactionBuilder builder = new TransactionBuilder("Set date and time"); + setTime(builder); + builder.queue(getQueue()); + } + + + public FitProDeviceSupport setTime(TransactionBuilder builder) { + LOG.debug("FitPro set time"); + Calendar calendar = Calendar.getInstance(); + + int datetime = calendar.get(Calendar.SECOND) | ( + (calendar.get(Calendar.YEAR) - 2000) << 26 | calendar.get(Calendar.MONTH) + 1 << 22 | + calendar.get(Calendar.DAY_OF_MONTH) << 17 | + calendar.get(Calendar.HOUR_OF_DAY) << 12 | calendar.get(Calendar.MINUTE) << 6); + + //this is how the values can be re-stored + // result is this + //byte[] array = new byte[]{(byte) (datetime >> 24), (byte) (datetime >> 16), (byte) (datetime >> 8), (byte) (datetime >> 0)}; + // int datetime2 = ByteBuffer.wrap(array).getInt(); + + //byte[] time = craftData(LT716Constants.CMD_SET_DATE_TIME, new byte[]{(byte) (datetime >> 24), (byte) (datetime >> 16), (byte) (datetime >> 8), (byte) (datetime >> 0)}); + byte[] time = craftData(CMD_GROUP_GENERAL, CMD_SET_DATE_TIME, (ByteBuffer.allocate(4).putInt(datetime).array())); + builder.write(writeCharacteristic, time); + return this; + } + + + @Override + public void onSetAlarms(ArrayList alarms) { + LOG.debug("FitPro set alarms"); + + // handle one-shot alarm from the widget: + // this device doesn't have concept of on-off alarm, so use the last slot for this and store + // this alarm in the database so the user knows what is going on and can disable it + + if (alarms.toArray().length == 1 && alarms.get(0).getRepetition() == 0) { + Alarm oneshot = alarms.get(0); + DBHandler db = null; + try { + db = GBApplication.acquireDB(); + DaoSession daoSession = db.getDaoSession(); + Device device = DBHelper.getDevice(gbDevice, daoSession); + User user = DBHelper.getUser(daoSession); + nodomain.freeyourgadget.gadgetbridge.entities.Alarm tmpAlarm = + new nodomain.freeyourgadget.gadgetbridge.entities.Alarm( + device.getId(), + user.getId(), + 7, + true, + false, + false, + 0, + oneshot.getHour(), + oneshot.getMinute(), + true, //kind of indicate the specialty of this alarm + "", + ""); + daoSession.insertOrReplace(tmpAlarm); + GBApplication.releaseDB(); + } catch (GBException e) { + LOG.error("error storing one shot quick alarm"); + } + } + + try { + TransactionBuilder builder = performInitialized("Set alarm"); + boolean anyAlarmEnabled = false; + byte[] all_alarms = new byte[]{}; + + for (Alarm alarm : alarms) { + Calendar calendar = AlarmUtils.toCalendar(alarm); + anyAlarmEnabled |= alarm.getEnabled(); + LOG.debug("alarms: " + alarm.getPosition()); + int maxAlarms = 8; + if (alarm.getPosition() >= maxAlarms) { //we should never encounter this, but just in case + if (alarm.getEnabled()) { + GB.toast(getContext(), "Only 8 alarms are supported.", Toast.LENGTH_LONG, GB.WARN); + } + return; + } + if (alarm.getEnabled()) { + long datetime = (long) alarm.getRepetition() | ( + (long) (calendar.get(Calendar.YEAR) - 2000) << 34 | + (long) (calendar.get(Calendar.MONTH) + 1) << 30 | + (long) (calendar.get(Calendar.DAY_OF_MONTH)) << 25 | + (long) (calendar.get(Calendar.HOUR_OF_DAY)) << 20 | + (long) (calendar.get(Calendar.MINUTE)) << 14 | + 1L << 11); + byte[] single_alarm = new byte[]{(byte) (datetime >> 32), (byte) (datetime >> 24), (byte) (datetime >> 16), (byte) (datetime >> 8), (byte) (datetime)}; + all_alarms = ArrayUtils.addAll(all_alarms, single_alarm); + } + } + + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_ALARM, all_alarms)); + 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 onReset(int flags) { + LOG.debug("FitPro reset flags: " + flags); + byte[] command = craftData(CMD_GROUP_RESET, CMD_RESET); + switch (flags) { + case 1: + command = craftData(CMD_GROUP_RESET, CMD_RESET); + break; + case 2: + command = craftData(CMD_GROUP_BIND, CMD_UNBIND); + break; + } + + getQueue().clear(); + TransactionBuilder builder = new TransactionBuilder("resetting"); + builder.write(writeCharacteristic, command); + builder.queue(getQueue()); + } + + @Override + public void onHeartRateTest() { + TransactionBuilder builder = new TransactionBuilder("notification"); + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_HEART_RATE_MEASUREMENT, VALUE_ON)); + builder.queue(getQueue()); + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + + } + + @Override + public void onFindDevice(boolean start) { + getQueue().clear(); + LOG.debug("FitPro find device"); + TransactionBuilder builder = new TransactionBuilder("searching"); + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_FIND_BAND, start ? VALUE_ON : VALUE_OFF)); + builder.queue(getQueue()); + } + + @Override + public void onSetConstantVibration(int integer) { + + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + + public FitProDeviceSupport setAutoHeartRate(TransactionBuilder builder) { + LOG.debug("FitPro set automatic heartrate measurements"); + boolean prefAutoheartrateSwitch = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean("pref_autoheartrate_switch", false); + LOG.info("Setting autoheartrate to " + prefAutoheartrateSwitch); + + boolean sleep = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean("pref_autoheartrate_sleep", false); + String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("pref_autoheartrate_start", "06:00"); + String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("pref_autoheartrate_end", "23:00"); + String interval = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("pref_autoheartrate_interval", "2"); + + int intervalInt = Integer.parseInt(interval); + int sleepInt = sleep ? 1 : 0; + int autoheartrateInt = prefAutoheartrateSwitch ? 1 : 0; + + Calendar startCalendar = GregorianCalendar.getInstance(); + Calendar endCalendar = GregorianCalendar.getInstance(); + DateFormat df = new SimpleDateFormat("HH:mm"); + + try { + startCalendar.setTime(df.parse(start)); + endCalendar.setTime(df.parse(end)); + } catch (ParseException e) { + LOG.error("settings error: " + e); + } + + int startTime = (startCalendar.get(Calendar.HOUR_OF_DAY) * 60) + startCalendar.get(Calendar.MINUTE); + int endTime = (endCalendar.get(Calendar.HOUR_OF_DAY) * 60) + endCalendar.get(Calendar.MINUTE); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(autoheartrateInt); + outputStream.write(sleepInt); + outputStream.write(intervalInt >> 8); + outputStream.write(intervalInt); + outputStream.write(startTime >> 8); + outputStream.write(startTime); + outputStream.write(endTime >> 8); + outputStream.write(endTime); + //outputStream.write(0x7F); + + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_GROUP_HEARTRATE_SETTINGS, outputStream.toByteArray())); + + return this; + } + + public FitProDeviceSupport setLongSitReminder(TransactionBuilder builder) { + LOG.debug("FitPro set inactivity warning"); + boolean prefLongsitSwitch = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean("pref_longsit_switch", false); + LOG.info("Setting long sit warning to " + prefLongsitSwitch); + + if (prefLongsitSwitch) { + + String inactivity = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("pref_longsit_period", "4"); + String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("pref_longsit_start", "08:00"); + String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("pref_longsit_end", "16:00"); + Calendar startCalendar = GregorianCalendar.getInstance(); + Calendar endCalendar = GregorianCalendar.getInstance(); + DateFormat df = new SimpleDateFormat("HH:mm"); + + try { + startCalendar.setTime(df.parse(start)); + endCalendar.setTime(df.parse(end)); + } catch (ParseException e) { + LOG.debug("settings error: " + e); + } + + int inactivityInt = Integer.parseInt(inactivity); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + outputStream.write(VALUE_SET_LONG_SIT_REMINDER_ON); + outputStream.write(inactivityInt); + outputStream.write(startCalendar.get(Calendar.HOUR_OF_DAY)); + outputStream.write(endCalendar.get(Calendar.HOUR_OF_DAY)); + outputStream.write(0x7F); + } catch (IOException e) { + LOG.error("settings error: " + e); + } + + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_LONG_SIT_REMINDER, outputStream.toByteArray())); + LOG.info("Setting long sit warning to scheduled"); + + } else { + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_LONG_SIT_REMINDER, VALUE_SET_LONG_SIT_REMINDER_OFF)); + LOG.info("Setting long sit warning to OFF"); + } + return this; + } + + public FitProDeviceSupport setDoNotDisturb(TransactionBuilder builder) { + LOG.debug("FitPro set DND"); + String dnd = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("do_not_disturb_no_auto", "off"); + LOG.info("Setting DND to " + dnd); + int dndInt = dnd.equals("scheduled") ? 1 : 0; + + String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("do_not_disturb_no_auto_start", "22:00"); + String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("do_not_disturb_no_auto_end", "06:00"); + + Calendar startCalendar = GregorianCalendar.getInstance(); + Calendar endCalendar = GregorianCalendar.getInstance(); + DateFormat df = new SimpleDateFormat("HH:mm"); + + try { + startCalendar.setTime(df.parse(start)); + endCalendar.setTime(df.parse(end)); + } catch (ParseException e) { + LOG.error("settings error: " + e); + } + + int startTime = (startCalendar.get(Calendar.HOUR_OF_DAY) * 60) + startCalendar.get(Calendar.MINUTE); + int endTime = (endCalendar.get(Calendar.HOUR_OF_DAY) * 60) + endCalendar.get(Calendar.MINUTE); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + outputStream.write(dndInt); + outputStream.write(startTime >> 8); + outputStream.write(startTime); + outputStream.write(endTime >> 8); + outputStream.write(endTime); + + debugPrintArray(craftData(CMD_GROUP_GENERAL, CMD_DND, outputStream.toByteArray()), "enable DND"); + debugPrintArray(outputStream.toByteArray(), "payload"); + LOG.info("Setting DND to scheduled: " + start + " " + end); + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_DND, outputStream.toByteArray())); + LOG.info("Setting DND scheduled"); + + return this; + } + + public FitProDeviceSupport setSleepTime(TransactionBuilder builder) { + LOG.debug("FitPro set sleep times"); + String sleepTime = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("prefs_enable_sleep_time", "off"); + LOG.info("Setting sleep times to " + sleepTime); + int sleepTimeInt = sleepTime.equals("scheduled") ? 1 : 0; + + String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("prefs_sleep_time_start", "22:00"); + String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("prefs_sleep_time_end", "06:00"); + + Calendar startCalendar = GregorianCalendar.getInstance(); + Calendar endCalendar = GregorianCalendar.getInstance(); + DateFormat df = new SimpleDateFormat("HH:mm"); + + try { + startCalendar.setTime(df.parse(start)); + endCalendar.setTime(df.parse(end)); + } catch (ParseException e) { + LOG.error("settings error: " + e); + } + + int startTime = (startCalendar.get(Calendar.HOUR_OF_DAY) * 60) + startCalendar.get(Calendar.MINUTE); + int endTime = (endCalendar.get(Calendar.HOUR_OF_DAY) * 60) + endCalendar.get(Calendar.MINUTE); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(sleepTimeInt); + outputStream.write(startTime >> 8); + outputStream.write(startTime); + outputStream.write(endTime >> 8); + outputStream.write(endTime); + debugPrintArray(craftData(CMD_GROUP_GENERAL, CMD_SET_SLEEP_TIMES, outputStream.toByteArray()), "enable sleep time"); + debugPrintArray(outputStream.toByteArray(), "payload"); + LOG.info("Setting sleep times scheduled: " + start + " " + end); + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_SLEEP_TIMES, outputStream.toByteArray())); + LOG.info("Setting sleep times scheduled"); + return this; + } + + public FitProDeviceSupport setWearLocation(TransactionBuilder builder) { + LOG.debug("FitPro set wearing location"); + byte location = VALUE_SET_ARM_LEFT; + String setLocation = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left"); + if ("right".equals(setLocation)) { + location = VALUE_SET_ARM_RIGHT; + } + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_ARM, location)); + return this; + } + + public FitProDeviceSupport setDisplayOnLift(TransactionBuilder builder) { + LOG.debug("FitPro set display on lift"); + String displayLift = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("activate_display_on_lift_wrist", "off"); + + int displayLiftInt = displayLift.equals("scheduled") ? 1 : 0; + + LOG.info("Setting activate display on lift wrist to:" + displayLift + ": " + displayLiftInt); + + String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("display_on_lift_start", "08:00"); + String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("display_on_lift_end", "16:00"); + + Calendar startCalendar = GregorianCalendar.getInstance(); + Calendar endCalendar = GregorianCalendar.getInstance(); + DateFormat df = new SimpleDateFormat("HH:mm"); + + try { + startCalendar.setTime(df.parse(start)); + endCalendar.setTime(df.parse(end)); + } catch (ParseException e) { + LOG.error("settings error: " + e); + } + + int startTime = (startCalendar.get(Calendar.HOUR_OF_DAY) * 60) + startCalendar.get(Calendar.MINUTE); + int endTime = (endCalendar.get(Calendar.HOUR_OF_DAY) * 60) + endCalendar.get(Calendar.MINUTE); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(displayLiftInt); + outputStream.write(startTime >> 8); + outputStream.write(startTime); + outputStream.write(endTime >> 8); + outputStream.write(endTime); + debugPrintArray(craftData(CMD_GROUP_GENERAL, CMD_SET_DISPLAY_ON_LIFT, outputStream.toByteArray()), "enable lift display"); + debugPrintArray(outputStream.toByteArray(), "payload"); + LOG.info("Setting activate display on lift wrist scheduled: " + start + " " + end); + builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_DISPLAY_ON_LIFT, outputStream.toByteArray())); + LOG.info("Setting activate display on lift wrist scheduled"); + + return this; + } + + public FitProDeviceSupport setStepsGoal(TransactionBuilder builder) { + LOG.debug("FitPro set step goal"); + //cd 00 09 12 01 03 00 04 00 00 05 dc + + ActivityUser activityUser = new ActivityUser(); + int stepGoal = activityUser.getStepsGoal(); + byte[] data = craftData(CMD_GROUP_GENERAL, CMD_SET_STEP_GOAL, ByteBuffer.allocate(4).putInt(stepGoal).array()); + + builder.write(writeCharacteristic, data); + return this; + } + + public void handleSleepData(byte[] value) { + debugPrintArray(value, "sleep data value"); + // sleep packet consists of: date + list of 4bytes of 15minutes intervals + // these intervals contain seconds offset from the date and type of sleep + byte[] dateArray = new byte[2]; + System.arraycopy(value, 8, dateArray, 0, 2); + Calendar date = decodeDateTime(dateArray); + List samples = new ArrayList<>(); + for (int i = 12; i < value.length - 3; i = i + 4) { + byte[] packet = new byte[4]; + System.arraycopy(value, i, packet, 0, 4); + int data = ByteBuffer.wrap(packet).getInt(); + int activity_kind = (int) (data & 0xff); + int encodedTime = (int) (data >> 16); + int seconds = getSleepSecondsOfDay(encodedTime); + Calendar now = (Calendar) date.clone(); // do not modify the caller's argument + now.add(Calendar.SECOND, seconds); + int timestamp = (int) (now.getTimeInMillis() / 1000L); + debugPrintArray(packet, "processing sleep packet"); + + LOG.debug("FitPro new sleep: " + activity_kind + " seconds: " + seconds + " ts: " + timestamp + " date: " + DateTimeUtils.formatDateTime(new Date(timestamp * 1000L))); + + FitProActivitySample sample = new FitProActivitySample(); + sample.setTimestamp(timestamp); + sample.setHeartRate(ActivitySample.NOT_MEASURED); + sample.setActiveTimeMinutes(15); + sample.setRawKind(rawSleepKindToUniqueKind(activity_kind)); + samples.add(sample); + } + if (addGBActivitySamples(samples)) { + sendAck(value[3], value[1], value[2], value[5]); + } + } + + public int rawSleepKindToUniqueKind(int kind) { + //step and sleep are the same kind so we must distinguish them + return kind + 10; + } + + public int rawActivityKindToUniqueKind(int kind) { + return kind; + } + + public void handleStepData(byte[] value) { + debugPrintArray(value, "step data value"); + // step packet consists of: date + list of 8bytes of (always?) 5minutes intervals + // these intervals contain seconds offset from the date, type of activity, calories, + // steps, distance, duration + + byte[] dateArray = new byte[2]; + System.arraycopy(value, 8, dateArray, 0, 2); + Calendar date = decodeDateTime(dateArray); + List samples = new ArrayList<>(); + for (int i = 12; i < value.length - 7; i = i + 8) { + byte[] packet = new byte[8]; + System.arraycopy(value, i, packet, 0, 8); + long data = ByteBuffer.wrap(packet).getLong(); + int steps = (int) Math.abs((data >> 52)); + int calories = (int) (data & 0x7ffff); + int activity_kind = (int) ((data >> 19) & 0x1); + int duration = (int) ((data >> 48) & 0xf); + int distance = (int) ((data >> 32) & 0xffff); + int encodedTime = (int) ((data >> 21) & 0x7ff); + int seconds = getSecondsOfDay(encodedTime); + Calendar now = (Calendar) date.clone(); // do not modify the caller's argument + now.add(Calendar.SECOND, seconds); + int timestamp = (int) (now.getTimeInMillis() / 1000L); + debugPrintArray(packet, "processing steps packet"); + LOG.debug("FitPro adding new steps: " + steps); + FitProActivitySample sample = new FitProActivitySample(); + sample.setTimestamp(timestamp); + sample.setHeartRate(ActivitySample.NOT_MEASURED); + sample.setSteps(steps); + sample.setDistanceMeters(distance); + sample.setCaloriesBurnt(calories); + sample.setActiveTimeMinutes(duration); + sample.setRawKind(rawActivityKindToUniqueKind(activity_kind)); + samples.add(sample); + } + if (addGBActivitySamples(samples)) { + sendAck(value[3], value[1], value[2], value[5]); + } + } + + public void addGBActivitySample(FitProActivitySample sample) { + List samples = new ArrayList<>(); + samples.add(sample); + addGBActivitySamples(samples); + } + + private boolean addGBActivitySamples(List samples) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + + User user = DBHelper.getUser(dbHandler.getDaoSession()); + Device device = DBHelper.getDevice(this.getDevice(), dbHandler.getDaoSession()); + FitProSampleProvider provider = new FitProSampleProvider(this.getDevice(), dbHandler.getDaoSession()); + + for (FitProActivitySample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + sample.setProvider(provider); + provider.addGBActivitySample(sample); + } + + } catch (Exception ex) { + LOG.error("Error saving samples: " + ex); + GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); + return false; + } + return true; + } + + public void broadcastSample(FitProActivitySample sample) { + Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) + .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + + public void handleHR(int seconds, int heartRate, int pressureLow, int pressureHigh, int spo2) { + LOG.debug("FitPro handle heart rate measurement"); + + Calendar date = Calendar.getInstance(); + date.set(Calendar.HOUR_OF_DAY, 0); + date.set(Calendar.MINUTE, 0); + date.set(Calendar.SECOND, 0); + date.add(Calendar.SECOND, seconds); + LOG.debug("date: " + date); + + FitProActivitySample sample = new FitProActivitySample(); + + sample.setHeartRate(heartRate); + sample.setPressureLowMmHg(pressureLow); + sample.setPressureHighMmHg(pressureHigh); + sample.setSpo2Percent(spo2); + + sample.setTimestamp((int) (date.getTimeInMillis() / 1000)); + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + + addGBActivitySample(sample); + broadcastSample(sample); + GB.signalActivityDataFinish(); + } + + public void handleDayTotalsData(int steps, int distance, int calories) { + //this is for day data values, not used in Gb, handleStepData uses the better, 5min data + LOG.debug("FitPro handle day total steps"); + + LOG.debug("Steps: " + steps); + LOG.debug("Distance: " + distance); + LOG.debug("Calories: " + calories); + + Calendar dateStart = Calendar.getInstance(); + dateStart.set(Calendar.HOUR_OF_DAY, 0); + dateStart.set(Calendar.MINUTE, 0); + dateStart.set(Calendar.SECOND, 0); + + Calendar dateEnd = Calendar.getInstance(); + dateEnd.set(Calendar.HOUR_OF_DAY, 23); + dateEnd.set(Calendar.MINUTE, 59); + dateEnd.set(Calendar.SECOND, 59); + + int dayStepCount = getStepsOnDay(dateStart, dateEnd); + int newSteps = (steps - dayStepCount); + LOG.debug("FitPro dayStepCount " + dayStepCount); + LOG.debug("FitPro new steps " + newSteps); + + /* + if (newSteps > 0) { + LOG.debug("FitPro adding new steps " + newSteps); + ShenTechActivitySample sample = new ShenTechActivitySample(); + Calendar date = Calendar.getInstance(); + sample.setTimestamp((int) (date.getTimeInMillis() / 1000)); + sample.setSteps(newSteps); + sample.setDistanceMeters(distance); + sample.setCaloriesBurnt(calories); + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + sample.setRawIntensity(1); + addGBActivitySample(sample); + broadcastSample(sample); + } + */ + } + + private int getStepsOnDay(Calendar dayStart, Calendar dayEnd) { + //this is for day data values, not used in Gb, handleStepData uses 5min data which is better + try (DBHandler dbHandler = GBApplication.acquireDB()) { + + FitProSampleProvider provider = new FitProSampleProvider(this.getDevice(), dbHandler.getDaoSession()); + + List samples = provider.getActivitySamples( + (int) (dayStart.getTimeInMillis() / 1000L), + (int) (dayEnd.getTimeInMillis() / 1000L)); + + int totalSteps = 0; + + for (FitProActivitySample sample : samples) { + totalSteps += sample.getSteps(); + } + + return totalSteps; + + } catch (Exception ex) { + LOG.error(ex.getMessage()); + return 0; + } + } + + public int getSecondsOfDay(int encodedTime) { + int hours = (int) Math.floor((encodedTime * 15) / 60); + int minutes = (encodedTime * 15) % 60; + int seconds = (hours * 3600) + (minutes * 60); + return seconds; + } + + public int getSleepSecondsOfDay(int encodedTime) { + int hours = (int) Math.floor(encodedTime / 60); + int minutes = encodedTime % 60; + int seconds = (hours * 3600) + (minutes * 60); + return seconds; + } + + public Calendar decodeDateTime(byte[] dateArray) { + debugPrintArray(dateArray, "array to decode to date time"); + short dateShort = ByteBuffer.wrap(dateArray).getShort(); + + int day = (dateShort & 0x1f); + int month = ((dateShort >> 5) & 0xf); + int year = ((dateShort >> 9) + 2000); + + Calendar date = GregorianCalendar.getInstance(); + date.set(year, month - 1, day, 0, 0, 0); + return date; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java index 8ae96d98a..adef42285 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java @@ -80,4 +80,18 @@ public class ArrayUtils { public static boolean startsWith(byte[] array, byte[] values) { return equals(array, values, 0); } + + /** + * Converts an array to string representation + * + * @param array the array to convert + * @return + */ + public static String arrayToString(byte[] array) { + StringBuilder stringBuilder = new StringBuilder(); + for (byte i : array) { + stringBuilder.append(String.format("0x%02X ", i)); + } + return stringBuilder.toString(); + } } 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 0d41add22..2ec3a4570 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -43,6 +43,7 @@ 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.fitpro.FitProDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator; @@ -304,7 +305,7 @@ public class DeviceHelper { result.add(new SMAQ2OSSCoordinator()); result.add(new UM25Coordinator()); result.add(new DomyosT540Cooridnator()); - + result.add(new FitProDeviceCoordinator()); return result; } diff --git a/app/src/main/res/drawable/ic_chair.xml b/app/src/main/res/drawable/ic_chair.xml new file mode 100644 index 000000000..abe9e952a --- /dev/null +++ b/app/src/main/res/drawable/ic_chair.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml new file mode 100644 index 000000000..dd3d1f3c0 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 8b1e83925..47708b399 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -52,6 +52,34 @@ sk + + @string/automatic + @string/simplified_chinese + @string/english + @string/turkish + @string/polish + @string/portuguese + @string/russian + @string/spanish + @string/german + @string/french + @string/czesh + + + + auto + zh_CN + en_US + tr_TR + pl_PL + pt_BR + ru_RU + es_ES + de_DE + fr_FR + cs_CZ + + @string/always @string/when_screen_off @@ -1724,4 +1752,28 @@ @string/p_menuitem_goal + + 45 + 60 + 75 + 90 + 105 + 120 + + + + 3 + 4 + 5 + 6 + 7 + 8 + + + + 1 + 2 + 3 + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df26294ed..4fcb23b87 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -876,6 +876,7 @@ Sony SWR12 Wasp-os SMA-Q2 OSS + FitPro Choose export location General @@ -1257,4 +1258,16 @@ Power saving Disable display updates while off wrist Disable hands movement while off wrist +Sleep times + Define sleep hours + Specifies times when sleep is registered + Enable vibrations + Enable notifications + Vibrations for calls, messages, notifications and more + Notifications for calls, messages and more + Automatic Heart Rate + Periodical Heart Rate measurements during the day and also while asleep + Automatic Heart Rate measurements + Take measurements during sleep + Frequency of measurements diff --git a/app/src/main/res/xml/devicesettings_autoheartrate.xml b/app/src/main/res/xml/devicesettings_autoheartrate.xml new file mode 100644 index 000000000..68bb46ee1 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_autoheartrate.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_donotdisturb_no_auto.xml b/app/src/main/res/xml/devicesettings_donotdisturb_no_auto.xml index 7043596b7..099ee7a65 100644 --- a/app/src/main/res/xml/devicesettings_donotdisturb_no_auto.xml +++ b/app/src/main/res/xml/devicesettings_donotdisturb_no_auto.xml @@ -19,7 +19,7 @@ android:title="@string/mi2_prefs_do_not_disturb" /> diff --git a/app/src/main/res/xml/devicesettings_fitpro.xml b/app/src/main/res/xml/devicesettings_fitpro.xml new file mode 100644 index 000000000..739f281e3 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_fitpro.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/xml/devicesettings_liftwrist_display_no_on.xml b/app/src/main/res/xml/devicesettings_liftwrist_display_no_on.xml new file mode 100644 index 000000000..c29a103df --- /dev/null +++ b/app/src/main/res/xml/devicesettings_liftwrist_display_no_on.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_longsit.xml b/app/src/main/res/xml/devicesettings_longsit.xml index 500e22b30..00c588f66 100644 --- a/app/src/main/res/xml/devicesettings_longsit.xml +++ b/app/src/main/res/xml/devicesettings_longsit.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_longsit_noshed.xml b/app/src/main/res/xml/devicesettings_longsit_noshed.xml index 71e346fdb..ef1471212 100644 --- a/app/src/main/res/xml/devicesettings_longsit_noshed.xml +++ b/app/src/main/res/xml/devicesettings_longsit_noshed.xml @@ -1,7 +1,7 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_sleep_time.xml b/app/src/main/res/xml/devicesettings_sleep_time.xml new file mode 100644 index 000000000..bacfc6691 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_sleep_time.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_vibrations_enable.xml b/app/src/main/res/xml/devicesettings_vibrations_enable.xml new file mode 100644 index 000000000..fcfacb414 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_vibrations_enable.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/FitProTests.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/FitProTests.java new file mode 100644 index 000000000..99f590ff9 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/FitProTests.java @@ -0,0 +1,50 @@ +package nodomain.freeyourgadget.gadgetbridge.test; + +import org.junit.Test; + +public class FitProTests extends TestBase { + // since unit tests seem to be broken in Android Studio these are not complete + // but but at least this provides some help for the future + // can be used this way: + + @Test + public void testSleepPacket() { + String sleepData = "cd002915010300242b1f0008000f0003001e0002002d0001003c0001004b0001005a00010069000200780002"; + + //handleSleepData(stringToByteArray(sleepData)); + } + + @Test + public void testStepPacket() { + //note different format of data and thus different stringWith0xToByteArray method + String stepData = "0xcd 0x00 0x31 0x15 0x01 0x02 0x00 0x2c 0x2b 0x34 0x00 0x05 0x80 0x45 0x07 0x1a 0x09 0xc8 0xbb 0x06 0x79 0x55 0x06 0x26 0x09 0xe8 0xb0 0xe4 0x71 0x15 0x05 0x9b 0x0a 0x08 0xa4 0xdc 0x7b 0x85 0x06 0x67 0x0a 0x28 0xb4 0x16 0x76 0x35 0x06 0x16 0x0a 0x48 0xac 0x57"; + + //handleStepData(stringWith0xToByteArray(stepData)); + } + + public byte[] stringToByteArray(String s) { + byte[] byteArray = new byte[s.length() / 2]; + String[] strBytes = new String[s.length() / 2]; + int k = 0; + for (int i = 0; i < s.length(); i = i + 2) { + int j = i + 2; + strBytes[k] = s.substring(i, j); + byteArray[k] = (byte) Integer.parseInt(strBytes[k], 16); + k++; + } + return byteArray; + } + + + public static byte[] stringWith0xToByteArray(String s) { + String[] split = s.split(" "); + int k = 0; + byte[] byteArray = new byte[split.length]; + for (String ch : split) { + byteArray[k] = (byte) Integer.parseInt(ch.split("x")[1], 16); + k++; + } + return byteArray; + } + +}