Compare commits

...

63 Commits

Author SHA1 Message Date
Arjan Schrijver
0258905b4a Moyoung: Implement syncing sleep data 2025-01-08 23:06:13 +01:00
Arjan Schrijver
938085b5fa Moyoung: Add support for world clocks 2025-01-08 22:41:54 +01:00
Arjan Schrijver
ff865fbc99 Moyoung: Change bonding style to lazy to allow phone calls through watch 2025-01-08 22:41:54 +01:00
Arjan Schrijver
2b4a2b702c Moyoung: Add inactivity reminder preference 2025-01-08 22:41:54 +01:00
Arjan Schrijver
d9e0e22aaa Moyoung: Add power saving mode preference 2025-01-08 22:41:54 +01:00
Arjan Schrijver
529daded7a Moyoung: Send volume level with music info 2025-01-08 22:41:54 +01:00
Arjan Schrijver
deb7da7a5a Moyoung: Add option for letting the device follow the phone DND setting 2025-01-08 22:41:54 +01:00
Arjan Schrijver
3b347b29fd Moyoung: Support sending, receiving and deleting alarms 2025-01-08 22:41:54 +01:00
Arjan Schrijver
69578bdf88 Moyoung: Fix DND and Lift Wrist settings 2025-01-08 22:41:54 +01:00
Arjan Schrijver
e8b11f2ae9 Moyoung: Support the Misirun C17 2025-01-08 22:41:54 +01:00
Arjan Schrijver
42a9ee4fdc Moyoung: Send weather location and sunrise/sunset 2025-01-08 22:41:54 +01:00
Arjan Schrijver
2368a96a07 Moyoung: Implement sending calendar items 2025-01-08 22:41:54 +01:00
Arjan Schrijver
5b2fd8100d Moyoung: Implement sending music info and state 2025-01-08 22:41:54 +01:00
Arjan Schrijver
6b9f447725 Moyoung: Fix malformed notifications when sender/title contains colon 2025-01-08 22:41:54 +01:00
Arjan Schrijver
6a555bcba3 Moyoung: Implement and improve several device settings 2025-01-08 22:41:54 +01:00
Arjan Schrijver
59cb0ee70f Moyoung: Add music and volume control 2025-01-08 22:41:54 +01:00
Arjan Schrijver
b3d09f3209 Moyoung: Implement HR measurement interval setting 2025-01-08 22:41:54 +01:00
Arjan Schrijver
0bb1db06df Moyoung: Implement language setting 2025-01-08 22:41:54 +01:00
Arjan Schrijver
fa070579be Moyoung: Fix HR history packet parsing and activity sample provider 2025-01-08 22:41:54 +01:00
Arjan Schrijver
c0878cd214 Moyoung: Send cached weather info on request 2025-01-08 22:41:54 +01:00
Arjan Schrijver
809840be19 Moyoung: Code and settings improvements 2025-01-08 22:41:54 +01:00
Arjan Schrijver
52687e12dd Moyoung: Improve notifications 2025-01-08 22:41:54 +01:00
Arjan Schrijver
a1b260a145 Moyoung: Support syncing historical HR measurements 2025-01-08 22:41:54 +01:00
Arjan Schrijver
8fdd770292 Moyoung: Fix weather forecast being one day off 2025-01-08 22:41:54 +01:00
Arjan Schrijver
fb708d8693 Moyoung: Add find my phone functionality 2025-01-08 22:41:54 +01:00
Arjan Schrijver
7fcfa26a8e Moyoung: Make fixed MTU device-specific 2025-01-08 22:41:54 +01:00
Arjan Schrijver
b8d036c6bc Moyoung: Fixes for settings, sync, logging, weather, live activity 2025-01-08 22:41:54 +01:00
Arjan Schrijver
33664d1e83 Moyoung: Improve logging 2025-01-08 22:41:54 +01:00
Arjan Schrijver
736bf11852 Moyoung: Persist received data in new tables 2025-01-08 22:41:54 +01:00
Arjan Schrijver
970f34a776 Moyoung: Split up (modernize) database tables 2025-01-08 22:41:54 +01:00
Arjan Schrijver
945cf07cc9 Moyoung: Modernize classes and methods and fix compiler errors 2025-01-08 22:41:54 +01:00
Arjan Schrijver
678d1514de Rename DaFit references to Moyoung to reflect the protocol used 2025-01-08 22:41:54 +01:00
krzys-h
d4ca64dbe3 Da Fit: Setting alarms 2025-01-08 22:41:54 +01:00
krzys-h
81afa942d4 Da Fit: Training data transfer 2025-01-08 22:41:54 +01:00
krzys-h
656c912e97 Da Fit: Add device settings 2025-01-08 22:41:54 +01:00
krzys-h
8c49f54374 Da Fit: Add weather sync 2025-01-08 22:41:54 +01:00
krzys-h
f3698976c9 Da Fit: Add activity fetching and logging 2025-01-08 22:41:54 +01:00
krzys-h
95cdcd80bf Da Fit: Add handling of heart rate, blood pressure and oxidation measurements 2025-01-08 22:41:54 +01:00
krzys-h
ee088c734e Da Fit: Add notification support 2025-01-08 22:41:53 +01:00
krzys-h
ae6983e2ad Da Fit: Add time sync 2025-01-08 22:41:53 +01:00
krzys-h
ca7d9e19af Da Fit: Add device support, reverse engineering notes and base protocol implementation 2025-01-08 22:41:53 +01:00
José Rebelo
c0883de546 Garmin Forerunner 45: Initial support 2025-01-06 18:05:53 +00:00
Me7c7
1a21f01071 Huawei: UTF-16 encoding support for reply. 2025-01-06 14:06:57 +02:00
Me7c7
b1cccae3ac Huawei: fix SMS reply from the watch 2025-01-06 13:21:21 +02:00
Arjan Schrijver
790e81a6f6 Add disabling battery optimizations to permissions screen 2025-01-06 09:18:18 +01:00
Me7c7
4ccf68af0a Huawei: Fix notifications handling without key 2025-01-05 14:41:27 +02:00
Simon Brand
87871a46e7 Use correct logger in some classes 2025-01-05 09:04:53 +00:00
Sebastian Dröge
a7d5fad2b7 Ignore dev.octoshrimpy.quik as another SMS application
See https://github.com/octoshrimpy/quik
2025-01-05 10:47:01 +02:00
José Rebelo
e134a3bfbb Remove stray fragment_weeksteps_chart landscape layout 2025-01-04 22:09:05 +00:00
José Rebelo
e81597eb3d Add option to hide sleep and steps balance (#2476) 2025-01-04 13:53:21 +00:00
José Rebelo
ef5f4d9fd0 Restore steps balance on weekly and monthly charts 2025-01-04 13:46:33 +00:00
José Rebelo
d432800ae4 Sony Headphones: Fix unit tests 2025-01-04 13:25:05 +00:00
José Rebelo
894f913a89 Update changelog 2025-01-04 13:13:48 +00:00
Me7c7
dd7d63fa07 Huawei: Set phone number to notification. 2025-01-04 13:07:14 +00:00
Me7c7
21f8b88746 Ignore org.fossify.messages. It is SMS 2025-01-04 13:07:14 +00:00
Me7c7
1450219351 Huawei: Allow to set and edit canned replies 2025-01-04 13:07:14 +00:00
Me7c7
8da2b68eed Huawei: reply only if quick actions allow it 2025-01-04 13:07:14 +00:00
Me7c7
b7641f6e45 Huawei: permission check request processing 2025-01-04 13:07:14 +00:00
Me7c7
86e32f0713 Huawei: fixes related to replies 2025-01-04 13:07:14 +00:00
Me7c7
0429c2f3c8 Add cahannel and category to notifcation 2025-01-04 13:07:14 +00:00
dependency-bot
61831b8a9d Update dependency org.json:json to v20241224 2025-01-04 13:03:37 +00:00
dependency-bot
92a76cfa7b Update dependency org.mockito:mockito-core to v5.15.2 2025-01-04 01:10:02 +00:00
~noodlez1232
fbd4cb810a AsteroidOS: Add volume control
This pull request adds volume control to the AsteroidOS implementation.
It's not quite what I wanted from it, but it's probably good enough.
Hopefully it's good enough for other people too
2025-01-03 12:19:17 -08:00
93 changed files with 5944 additions and 233 deletions

View File

@ -1,8 +1,23 @@
### Changelog
#### NEXT
* Marstek B2500: Fix setting pass-though mode
* Initial support for Redmi Buds 6 Active
* Initial support for Sony WF-C510
* AsteroidOS: Add volume control
* Colmi R09: Add preference to toggle temperature measurements
* Colmi R09: Fix temperature data parsing
* Colmi R0x: Add support for realtime heart rate meassurements and live activity tracking
* Huawei: Add support to set and use canned replies
* Huawei: Fix calendar event updates
* Huawei: match midnight on the user's timezone for all day events
* Huawei: Remove notifications from watch
* Marstek B2500: Display sensor temperature in Status Activity
* Marstek B2500: Fix setting pass-though mode
* Sony Headphones: Allow overriding supported features
* Sony Headphones: Fix initialization for some devices
* Sony Headphones: Update default low battery threshold
* Recognize Fossify SMS as SMS
* Limit live activity to just the current device
#### 0.83.1
* Initial support for Garmin Fenix 6X Pro Solar

View File

@ -49,12 +49,14 @@ public class GBDaoGenerator {
private static final String SAMPLE_TEMPERATURE = "temperature";
private static final String SAMPLE_TEMPERATURE_TYPE = "temperatureType";
private static final String SAMPLE_WEIGHT_KG = "weightKg";
private static final String SAMPLE_BLOOD_PRESSURE_SYSTOLIC = "bpSystolic";
private static final String SAMPLE_BLOOD_PRESSURE_DIASTOLIC = "bpDiastolic";
private static final String TIMESTAMP_FROM = "timestampFrom";
private static final String TIMESTAMP_TO = "timestampTo";
public static void main(String[] args) throws Exception {
final Schema schema = new Schema(93, MAIN_PACKAGE + ".entities");
final Schema schema = new Schema(94, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@ -151,6 +153,11 @@ public class GBDaoGenerator {
addColmiHrvValueSample(schema, user, device);
addColmiHrvSummarySample(schema, user, device);
addColmiTemperatureSample(schema, user, device);
addMoyoungActivitySample(schema, user, device);
addMoyoungHeartRateSample(schema, user, device);
addMoyoungSpo2Sample(schema, user, device);
addMoyoungBloodPressureSample(schema, user, device);
addMoyoungSleepStageSample(schema, user, device);
addHuaweiActivitySample(schema, user, device);
@ -603,6 +610,11 @@ public class GBDaoGenerator {
activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE);
}
private static void addBloodPressureProperies(Entity activitySample) {
activitySample.addIntProperty(SAMPLE_BLOOD_PRESSURE_SYSTOLIC).notNull();
activitySample.addIntProperty(SAMPLE_BLOOD_PRESSURE_DIASTOLIC).notNull();
}
private static Entity addPebbleHealthActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "PebbleHealthActivitySample");
addCommonActivitySampleProperties("AbstractPebbleHealthActivitySample", activitySample, user, device);
@ -1057,6 +1069,48 @@ public class GBDaoGenerator {
return activitySample;
}
private static Entity addMoyoungActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "MoyoungActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty("dataSource").notNull();
activitySample.addIntProperty("caloriesBurnt").notNull();
activitySample.addIntProperty("distanceMeters").notNull();
addHeartRateProperties(activitySample);
return activitySample;
}
private static Entity addMoyoungHeartRateSample(Schema schema, Entity user, Entity device) {
Entity heartRateSample = addEntity(schema, "MoyoungHeartRateSample");
heartRateSample.implementsSerializable();
addCommonTimeSampleProperties("AbstractHeartRateSample", heartRateSample, user, device);
heartRateSample.addIntProperty(SAMPLE_HEART_RATE).notNull();
return heartRateSample;
}
private static Entity addMoyoungSpo2Sample(Schema schema, Entity user, Entity device) {
Entity spo2sample = addEntity(schema, "MoyoungSpo2Sample");
addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device);
spo2sample.addIntProperty("spo2").notNull().codeBeforeGetter(OVERRIDE);
return spo2sample;
}
private static Entity addMoyoungBloodPressureSample(Schema schema, Entity user, Entity device) {
Entity bpSample = addEntity(schema, "MoyoungBloodPressureSample");
addCommonTimeSampleProperties("AbstractBloodPressureSample", bpSample, user, device);
addBloodPressureProperies(bpSample);
return bpSample;
}
private static Entity addMoyoungSleepStageSample(Schema schema, Entity user, Entity device) {
Entity sleepStageSample = addEntity(schema, "MoyoungSleepStageSample");
addCommonTimeSampleProperties("AbstractTimeSample", sleepStageSample, user, device);
sleepStageSample.addIntProperty("stage").notNull();
return sleepStageSample;
}
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
activitySample.setSuperclass(superClass);
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");

View File

@ -207,7 +207,7 @@ dependencies {
implementation 'androidx.camera:camera-lifecycle:1.4.1'
testImplementation "junit:junit:4.13.2"
testImplementation "org.mockito:mockito-core:5.14.2"
testImplementation "org.mockito:mockito-core:5.15.2"
testImplementation "org.robolectric:robolectric:4.14.1"
testImplementation "org.hamcrest:hamcrest-library:3.0"
@ -254,7 +254,7 @@ dependencies {
//implementation 'org.bouncycastle:bcprov-jdk18on:1.76'
// Android SDK bundles org.json, but we need an actual implementation to replace the stubs in tests
testImplementation 'org.json:json:20240303'
testImplementation 'org.json:json:20241224'
// Fix Duplicate class build error for conflicting kotlin-stdlib versions
// does not seem to be currently needed, as it uses the latest across all transitive

View File

@ -38,6 +38,13 @@
<!-- Take wake locks (e.g. for time sync) -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!--
Necessary for asking the user to disable battery optimizations.
GB falls under the acceptable use cases documented here:
https://developer.android.com/training/monitoring-device-state/doze-standby.html#exemption-cases
-->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<!-- Read loyalty cards from Catima -->
<uses-permission android:name="me.hackerchick.catima.READ_CARDS"/>
<uses-permission android:name="me.hackerchick.catima.debug.READ_CARDS"/>

View File

@ -19,12 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.app.Activity;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.components.LimitLine;
@ -68,9 +62,8 @@ public abstract class AbstractWeekChartFragment extends AbstractActivityChartFra
protected int mTargetValue = 0;
protected BarChart mWeekChart;
protected TextView mBalanceView;
private int mOffsetHours = getOffsetHours();
private final int mOffsetHours = getOffsetHours();
protected String getWeeksChartsLabel(Calendar day){
if (TOTAL_DAYS > 7) {
@ -210,35 +203,6 @@ public abstract class AbstractWeekChartFragment extends AbstractActivityChartFra
return lineDataSet;
};
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mLocale = getResources().getConfiguration().locale;
View rootView = inflater.inflate(R.layout.fragment_weeksteps_chart, container, false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
getChartsHost().enableSwipeRefresh(scrollY == 0);
});
}
final int goal = getGoal();
if (goal >= 0) {
mTargetValue = goal;
}
mWeekChart = rootView.findViewById(R.id.weekstepschart);
mBalanceView = rootView.findViewById(R.id.balance);
setupWeekChart();
// refresh immediately instead of use refreshIfVisible(), for perceived performance
refresh();
return rootView;
}
protected void setupWeekChart() {
mWeekChart.setBackgroundColor(BACKGROUND_COLOR);
mWeekChart.getDescription().setTextColor(DESCRIPTION_COLOR);

View File

@ -31,7 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.AbstractRespiratoryRateSamp
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class RespiratoryRateDailyFragment extends RespiratoryRateFragment<RespiratoryRateFragment.RespiratoryRateDay> {
protected static final Logger LOG = LoggerFactory.getLogger(BodyEnergyFragment.class);
protected static final Logger LOG = LoggerFactory.getLogger(RespiratoryRateDailyFragment.class);
private TextView mDateView;
private TextView sleepAvg;

View File

@ -35,7 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class RespiratoryRatePeriodFragment extends RespiratoryRateFragment<RespiratoryRatePeriodFragment.RespiratoryRateData> {
protected static final Logger LOG = LoggerFactory.getLogger(BodyEnergyFragment.class);
protected static final Logger LOG = LoggerFactory.getLogger(RespiratoryRatePeriodFragment.class);
private TextView mDateView;
private TextView sleepAvg;

View File

@ -45,7 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
public class StepsDailyFragment extends StepsFragment<StepsDailyFragment.StepsData> {
protected static final Logger LOG = LoggerFactory.getLogger(BodyEnergyFragment.class);
protected static final Logger LOG = LoggerFactory.getLogger(StepsDailyFragment.class);
private TextView mDateView;
private ImageView stepsGauge;

View File

@ -21,7 +21,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
abstract class StepsFragment<T extends ChartsData> extends AbstractChartFragment<T> {
protected static final Logger LOG = LoggerFactory.getLogger(StepsDailyFragment.class);
protected static final Logger LOG = LoggerFactory.getLogger(StepsFragment.class);
protected int CHART_TEXT_COLOR;
protected int TEXT_COLOR;

View File

@ -1,5 +1,6 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
@ -37,7 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.StepsData> {
protected static final Logger LOG = LoggerFactory.getLogger(BodyEnergyFragment.class);
protected static final Logger LOG = LoggerFactory.getLogger(StepsPeriodFragment.class);
private TextView mDateView;
private TextView stepsAvg;
@ -46,9 +47,12 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
private TextView distanceTotal;
private BarChart stepsChart;
private TextView mBalanceView;
protected int CHART_TEXT_COLOR;
protected int TEXT_COLOR;
protected int STEPS_GOAL;
protected boolean SHOW_BALANCE;
protected int BACKGROUND_COLOR;
protected int DESCRIPTION_COLOR;
@ -84,6 +88,16 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
stepsTotal = rootView.findViewById(R.id.steps_total);
distanceTotal = rootView.findViewById(R.id.distance_total);
STEPS_GOAL = GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_STEPS_GOAL, ActivityUser.defaultUserStepsGoal);
mBalanceView = rootView.findViewById(R.id.balance);
SHOW_BALANCE = GBApplication.getPrefs().getBoolean("charts_show_balance_steps", true);
if (SHOW_BALANCE) {
mBalanceView.setVisibility(View.VISIBLE);
} else {
mBalanceView.setVisibility(View.GONE);
}
setupStepsChart();
refresh();
@ -183,6 +197,8 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
distanceAvg.setText(valueFormatter.formatValue(stepsData.distanceDailyAvg, "km"));
stepsTotal.setText(String.format(String.valueOf(stepsData.totalSteps)));
distanceTotal.setText(valueFormatter.formatValue(stepsData.totalDistance, "km"));
mBalanceView.setText(stepsData.getBalanceMessage(getContext(), STEPS_GOAL));
}
ValueFormatter getStepsChartDayValueFormatter(StepsPeriodFragment.StepsData stepsData) {
@ -202,7 +218,8 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
stepsChart.invalidate();
}
protected void setupLegend(Chart<?> chart) {}
protected void setupLegend(Chart<?> chart) {
}
protected static class StepsData extends ChartsData {
List<StepsDay> days;
@ -211,6 +228,7 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
long totalSteps = 0;
double totalDistance = 0;
StepsDay todayStepsDay;
protected StepsData(List<StepsDay> days) {
this.days = days;
int daysCounter = 0;
@ -227,5 +245,19 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
}
this.todayStepsDay = days.get(days.size() - 1);
}
protected String getBalanceMessage(final Context context, final int targetValue) {
if (totalSteps == 0) {
return context.getString(R.string.no_data);
}
final long totalBalance = totalSteps - ((long) targetValue * days.size());
if (totalBalance > 0) {
return context.getString(R.string.overstep, Math.abs(totalBalance));
} else {
return context.getString(R.string.lack_of_step, Math.abs(totalBalance));
}
}
}
}

View File

@ -71,7 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class StressChartFragment extends AbstractChartFragment<StressChartFragment.StressChartsData> {
protected static final Logger LOG = LoggerFactory.getLogger(ActivitySleepChartFragment.class);
protected static final Logger LOG = LoggerFactory.getLogger(StressChartFragment.class);
private LineChart mStressChart;
private PieChart mStressLevelsPieChart;

View File

@ -67,6 +67,10 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
private LinearLayout sleepScoreWrapper;
private LineChart sleepScoreChart;
private TextView mBalanceView;
protected boolean SHOW_BALANCE;
public static WeekSleepChartFragment newInstance ( int totalDays ) {
WeekSleepChartFragment fragmentFirst = new WeekSleepChartFragment();
Bundle args = new Bundle();
@ -143,6 +147,13 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
mBalanceView = rootView.findViewById(R.id.balance);
SHOW_BALANCE = GBApplication.getPrefs().getBoolean("charts_show_balance_sleep", true);
if (SHOW_BALANCE) {
mBalanceView.setVisibility(View.VISIBLE);
} else {
mBalanceView.setVisibility(View.GONE);
}
if (!supportsSleepScore()) {
sleepScoreWrapper.setVisibility(View.GONE);
} else {

View File

@ -192,6 +192,7 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_INACTIVITY_DND = "inactivity_warnings_dnd";
public static final String PREF_INACTIVITY_DND_START = "inactivity_warnings_dnd_start";
public static final String PREF_INACTIVITY_DND_END = "inactivity_warnings_dnd_end";
public static final String PREF_INACTIVITY_STEPS = "inactivity_warnings_steps";
public static final String PREF_HEARTRATE_USE_FOR_SLEEP_DETECTION = "heartrate_sleep_detection";
public static final String PREF_HEARTRATE_MEASUREMENT_INTERVAL = "heartrate_measurement_interval";
@ -226,6 +227,8 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_DO_NOT_DISTURB_END = "do_not_disturb_end";
public static final String PREF_DO_NOT_DISTURB_LIFT_WRIST = "do_not_disturb_lift_wrist";
public static final String PREF_DO_NOT_DISTURB_NOT_WEAR = "do_not_disturb_not_wear";
public static final String PREF_DO_NOT_DISTURB_BOOL = "do_not_disturb_on_off";
public static final String PREF_DO_NOT_DISTURB_FOLLOW_PHONE = "do_not_disturb_follow_phone";
public static final String PREF_DO_NOT_DISTURB_OFF = "off";
public static final String PREF_DO_NOT_DISTURB_AUTOMATIC = "automatic";
public static final String PREF_DO_NOT_DISTURB_ALWAYS = "always";

View File

@ -35,6 +35,8 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PR
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_SCHEDULED;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_SWIPE_UNLOCK;
import static nodomain.freeyourgadget.gadgetbridge.devices.moyoung.MoyoungConstants.PREF_MOYOUNG_DEVICE_VERSION;
import static nodomain.freeyourgadget.gadgetbridge.devices.moyoung.MoyoungConstants.PREF_MOYOUNG_WATCH_FACE;
import android.content.Context;
import android.content.Intent;
@ -570,6 +572,8 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_SU);
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_LIFT_WRIST);
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOT_WEAR);
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_BOOL);
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_FOLLOW_PHONE);
addPreferenceHandlerFor(PREF_FIND_PHONE);
addPreferenceHandlerFor(PREF_FIND_PHONE_DURATION);
addPreferenceHandlerFor(PREF_AUTOLIGHT);
@ -814,6 +818,9 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_FEMOMETER_MEASUREMENT_MODE);
addPreferenceHandlerFor(PREF_MOYOUNG_WATCH_FACE);
addPreferenceHandlerFor(PREF_MOYOUNG_DEVICE_VERSION);
addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL);
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL);
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL_NOTIFICATION);

View File

@ -45,6 +45,7 @@ public class HeartRateCapability {
MINUTES_5(300, R.string.interval_five_minutes),
MINUTES_10(600, R.string.interval_ten_minutes),
MINUTES_15(900, R.string.interval_fifteen_minutes),
MINUTES_20(1200, R.string.interval_twenty_minutes),
MINUTES_30(1800, R.string.interval_thirty_minutes),
MINUTES_45(2700, R.string.interval_forty_five_minutes),
HOUR_1(3600, R.string.interval_one_hour),

View File

@ -16,6 +16,9 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.asteroidos;
import android.content.Context;
import android.media.AudioManager;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
/**
@ -29,12 +32,18 @@ public class AsteroidOSMediaCommand {
public static final byte COMMAND_VOLUME = 0x4;
public byte command;
public AsteroidOSMediaCommand(byte value) {
command = value;
public byte[] raw_values;
public Context context;
public AsteroidOSMediaCommand(byte[] values, Context device_context) {
command = values[0];
raw_values = values;
context = device_context;
}
/**
* Convert the MediaCommand to a music control event
*
* @return the matching music control event
*/
public GBDeviceEventMusicControl toMusicControlEvent() {
@ -53,9 +62,21 @@ public class AsteroidOSMediaCommand {
event.event = GBDeviceEventMusicControl.Event.PAUSE;
break;
case COMMAND_VOLUME:
setVolume(raw_values[1]);
event = null;
break;
default:
event.event = GBDeviceEventMusicControl.Event.UNKNOWN;
}
return event;
}
private void setVolume(byte volume) {
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
final int volumeMax = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
final int finalVol = (int) Math.round((volume * volumeMax) / 100f);
if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) != finalVol)
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (int) Math.round((volume * volumeMax) / 100f), AudioManager.FLAG_SHOW_UI);
}
}

View File

@ -0,0 +1,34 @@
/* Copyright (C) 2024 José Rebelo
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
public class GarminForerunner45Coordinator extends GarminCoordinator {
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("^Forerunner 45$");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_garmin_forerunner_45;
}
}

View File

@ -40,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
@ -153,6 +154,11 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
return huaweiCoordinator.getContactsSlotCount(device);
}
@Override
public int getCannedRepliesSlotCount(GBDevice device) {
return huaweiCoordinator.getCannedRepliesSlotCount(device);
}
@Override
public boolean supportsCalendarEvents() {
return huaweiCoordinator.supportsCalendarEvents();

View File

@ -55,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
@ -282,11 +283,18 @@ public class HuaweiCoordinator {
// Notifications
final List<Integer> notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.NOTIFICATIONS);
notifications.add(R.xml.devicesettings_notifications_enable);
if (supportsNotificationsRepeatedNotify() || supportsNotificationsRemoveSingle()){
notifications.add(R.xml.devicesettings_autoremove_notifications);
}
if (getCannedRepliesSlotCount(device) > 0) {
notifications.add(R.xml.devicesettings_canned_reply_16);
}
if (supportsNotificationOnBluetoothLoss())
notifications.add(R.xml.devicesettings_disconnectnotification_noshed);
if (supportsDoNotDisturb(device))
notifications.add(R.xml.devicesettings_donotdisturb_allday_liftwirst_notwear);
// Workout
if (supportsSendingGps())
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.WORKOUT, R.xml.devicesettings_workout_send_gps_to_band);
@ -660,7 +668,7 @@ public class HuaweiCoordinator {
return false;
}
public boolean supportsNotificationsReply() {
public boolean supportsNotificationsReplyActions() {
if (supportsExpandCapability())
return supportsExpandCapability(73);
return false;
@ -672,18 +680,23 @@ public class HuaweiCoordinator {
return false;
}
public boolean supportsNotificationsSyncKey() {
public boolean supportsNotificationsReply() {
if (supportsExpandCapability())
return supportsExpandCapability(89);
return false;
}
public boolean supportsNotificationsRemoveSingle() {
if (supportsExpandCapability())
return supportsExpandCapability(120);
return false;
}
public boolean supportsCannedReplies() {
if (supportsExpandCapability())
return supportsExpandCapability(82);
return false;
}
public boolean supportsPromptPushMessage () {
// do not ask for capabilities under specific condition
@ -754,6 +767,11 @@ public class HuaweiCoordinator {
return supportsContacts()?maxContactsCount:0;
}
public int getCannedRepliesSlotCount(GBDevice device) {
// TODO: find proper count
return supportsCannedReplies()?10:0;
}
public void setTransactionCrypted(boolean crypted) {
this.transactionCrypted = crypted;
}

View File

@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
@ -162,6 +163,11 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
return huaweiCoordinator.getContactsSlotCount(device);
}
@Override
public int getCannedRepliesSlotCount(GBDevice device) {
return huaweiCoordinator.getCannedRepliesSlotCount(device);
}
@Override
public boolean supportsCalendarEvents() {
return huaweiCoordinator.supportsCalendarEvents();

View File

@ -463,6 +463,8 @@ public class HuaweiPacket {
return new DeviceConfig.ActivityType.Response(paramsProvider).fromPacket(this);
case DeviceConfig.SettingRelated.id:
return new DeviceConfig.SettingRelated.Response(paramsProvider).fromPacket(this);
case DeviceConfig.PermissionCheck.id:
return new DeviceConfig.PermissionCheck.PermissionCheckRequest(paramsProvider).fromPacket(this);
case DeviceConfig.SecurityNegotiation.id:
return new DeviceConfig.SecurityNegotiation.Response(paramsProvider).fromPacket(this);
case DeviceConfig.WearStatus.id:

View File

@ -18,18 +18,11 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.TreeMap;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.json.JSONException;
import org.json.JSONObject;
@ -1688,6 +1681,48 @@ public class DeviceConfig {
}
public static class PermissionCheck {
public static final byte id = 0x38;
// NOTE: request from the watch
public static class PermissionCheckRequest extends HuaweiPacket {
public short permission = 0;
public PermissionCheckRequest(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = DeviceConfig.id;
this.commandId = id;
}
@Override
public void parseTlv() throws ParseException {
if (this.tlv.contains(0x01))
this.permission = this.tlv.getShort(0x01);
}
}
public static class PermissionCheckResponse extends HuaweiPacket {
public PermissionCheckResponse(
ParamsProvider paramsProvider,
short permission,
short status
) {
super(paramsProvider);
this.serviceId = DeviceConfig.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, permission)
.put(0x02, status);
this.complete = true;
}
}
}
public static class WearStatus {
public static final int id = 0x3D;

View File

@ -19,6 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
import android.text.TextUtils;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
@ -37,19 +39,19 @@ public class Notifications {
public static class AdditionalParams {
public boolean supportsSyncKey = false;
public boolean supportsReply = false;
public boolean supportsRepeatedNotify = false;
public boolean supportsRemoveSingle = false;
public boolean supportsReply = false;
public boolean supportsReplyActions = false;
public boolean supportsTimestamp = false;
public String replyKey = "";
public String notificationKey = "";
public int notificationId = -1;
public String channelId = "";
public byte subscriptionId = 0;
public String address = "";
public String category = "";
}
// TODO: support other types of notifications
@ -126,29 +128,25 @@ public class Notifications {
this.tlv.put(0x11, sourceAppId);
if(addParams != null) {
if (addParams.supportsSyncKey)
this.tlv.put(0x18, (addParams.notificationKey != null) ? addParams.notificationKey : "");
//this.tlv.put(0x12, "msg"); //"msg" or "imcall", maybe other - category, if not empty and productType>=34
//if(addParams.repeatedNotifySupports) {
// this.tlv.put(0x13, 0); // 0x13 - reminder 15 = vibrate, 0 - default
//}
if (addParams.supportsReply && notificationType == NotificationType.sms) {
if(!TextUtils.isEmpty(addParams.category)) { // type >= 34
this.tlv.put(0x12, addParams.category); // "imcall" also possible value, not standard for android
}
if (addParams.supportsReply) {
this.tlv.put(0x18, (addParams.replyKey != null) ? addParams.replyKey : "");
}
if (addParams.supportsReplyActions && notificationType == NotificationType.sms) {
this.tlv.put(0x14, addParams.subscriptionId);
this.tlv.put(0x17, addParams.address);
}
if (addParams.supportsRepeatedNotify || addParams.supportsRemoveSingle) {
this.tlv.put(0x19, (addParams.notificationKey != null) ? addParams.notificationKey : "");
this.tlv.put(0x20, addParams.notificationId);
this.tlv.put(0x1d, (addParams.channelId != null) ? addParams.channelId : "");
}
if (addParams.supportsTimestamp) {
this.tlv.put(0x15, (int) (System.currentTimeMillis() / 1000));
}
if (addParams.supportsRepeatedNotify || addParams.supportsRemoveSingle) {
this.tlv.put(0x19, (addParams.notificationKey != null) ? addParams.notificationKey : "");
this.tlv.put(0x1a, addParams.notificationId);
this.tlv.put(0x1b, (addParams.channelId != null) ? addParams.channelId : "");
}
}
this.complete = true;
@ -347,8 +345,8 @@ public class Notifications {
.put(0x03, notificationKey)
.put(0x04, notificationId)
.put(0x05, notificationChannelId);
if (notificationCategory != null && !TextUtils.isEmpty(notificationCategory))
this.tlv.put(0x06, notificationCategory); // category
if (!TextUtils.isEmpty(notificationCategory))
this.tlv.put(0x06, notificationCategory);
this.complete = true;
}
@ -380,14 +378,13 @@ public class Notifications {
public int type = 0;
public int encoding = 0; // 3 - "utf-16"
public int subId = 0;
public String sender;
public String key;
public String addData;
public String text;
public ReplyResponse(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = MusicControl.id;
this.serviceId = Notifications.id;
this.commandId = id;
}
@ -402,13 +399,17 @@ public class Notifications {
if (this.tlv.contains(0x04))
this.key = this.tlv.getString(0x04);
if (this.tlv.contains(0x05))
this.sender = this.tlv.getString(0x05);
if (this.tlv.contains(0x06))
this.addData = this.tlv.getString(0x05);
if (this.tlv.contains(0x06)) {
if(this.encoding == 3) {
this.text = new String(this.tlv.getBytes(0x06), StandardCharsets.UTF_16);
} else {
this.text = this.tlv.getString(0x06);
}
}
}
}
//TODO: send ack if required, 7f on error.
public static class ReplyAck extends HuaweiPacket {
public ReplyAck(
@ -426,9 +427,5 @@ public class Notifications {
this.complete = true;
}
}
}
}

View File

@ -0,0 +1,260 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung;
import android.annotation.TargetApi;
import android.bluetooth.le.ScanFilter;
import android.os.Build;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumDeviceVersion;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumMetricSystem;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumTimeSystem;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungSetting;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungSettingBool;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungSettingByte;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungSettingEnum;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungSettingInt;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungSettingLanguage;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungSettingRemindersToMove;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungSettingTimeRange;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungSettingUserInfo;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungBloodPressureSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungHeartRateSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSpo2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.moyoung.MoyoungDeviceSupport;
public abstract class AbstractMoyoungDeviceCoordinator extends AbstractBLEDeviceCoordinator {
@NonNull
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public Collection<? extends ScanFilter> createBLEScanFilters() {
ParcelUuid service = new ParcelUuid(MoyoungConstants.UUID_SERVICE_MOYOUNG);
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(service).build();
return Collections.singletonList(filter);
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return MoyoungDeviceSupport.class;
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_LAZY;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
Long deviceId = device.getId();
QueryBuilder<?> qb;
qb = session.getMoyoungActivitySampleDao().queryBuilder();
qb.where(MoyoungActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getMoyoungHeartRateSampleDao().queryBuilder();
qb.where(MoyoungHeartRateSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getMoyoungSpo2SampleDao().queryBuilder();
qb.where(MoyoungSpo2SampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getMoyoungBloodPressureSampleDao().queryBuilder();
qb.where(MoyoungBloodPressureSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
}
@Override
public boolean supportsActivityDataFetching() {
return true;
}
@Override
public boolean supportsActivityTracking() {
return true;
}
@Override
public boolean supportsSpo2(GBDevice device) {
return true;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new MoyoungActivitySampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
return new MoyoungSpo2SampleProvider(device, session);
}
@Override
public int getAlarmSlotCount(GBDevice device) {
return 3;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
}
@Override
public boolean supportsCalendarEvents() {
return true;
}
@Override
public boolean supportsRealtimeData() {
return true;
}
@Override
public boolean supportsWeather() {
return true;
}
@Override
public boolean supportsFindDevice() {
return true;
}
@Override
public boolean supportsActivityTracks() {
return true;
}
@Override
public boolean supportsMusicInfo() {
return true;
}
private static final MoyoungSetting[] MOYOUNG_SETTINGS = {
new MoyoungSettingUserInfo("USER_INFO", MoyoungConstants.CMD_SET_USER_INFO),
new MoyoungSettingByte("STEP_LENGTH", (byte)-1, MoyoungConstants.CMD_SET_STEP_LENGTH),
// (*) new MoyoungSettingEnum<>("DOMINANT_HAND", MoyoungConstants.CMD_QUERY_DOMINANT_HAND, MoyoungConstants.CMD_SET_DOMINANT_HAND, MoyoungEnumDominantHand.class),
new MoyoungSettingInt("GOAL_STEP", MoyoungConstants.CMD_QUERY_GOAL_STEP, MoyoungConstants.CMD_SET_GOAL_STEP),
new MoyoungSettingByte("HR_AUTO_INTERVAL", MoyoungConstants.CMD_QUERY_TIMING_MEASURE_HEART_RATE, MoyoungConstants.CMD_SET_TIMING_MEASURE_HEART_RATE),
new MoyoungSettingEnum<>("DEVICE_VERSION", MoyoungConstants.CMD_QUERY_DEVICE_VERSION, MoyoungConstants.CMD_SET_DEVICE_VERSION, MoyoungEnumDeviceVersion.class),
new MoyoungSettingLanguage("DEVICE_LANGUAGE", MoyoungConstants.CMD_QUERY_DEVICE_LANGUAGE, MoyoungConstants.CMD_SET_DEVICE_LANGUAGE),
new MoyoungSettingEnum<>("TIME_SYSTEM", MoyoungConstants.CMD_QUERY_TIME_SYSTEM, MoyoungConstants.CMD_SET_TIME_SYSTEM, MoyoungEnumTimeSystem.class),
new MoyoungSettingEnum<>("METRIC_SYSTEM", MoyoungConstants.CMD_QUERY_METRIC_SYSTEM, MoyoungConstants.CMD_SET_METRIC_SYSTEM, MoyoungEnumMetricSystem.class),
// (*) new MoyoungSetting("DISPLAY_DEVICE_FUNCTION", MoyoungConstants.CMD_QUERY_DISPLAY_DEVICE_FUNCTION, MoyoungConstants.CMD_SET_DISPLAY_DEVICE_FUNCTION),
// (*) new MoyoungSetting("SUPPORT_WATCH_FACE", MoyoungConstants.CMD_QUERY_SUPPORT_WATCH_FACE, (byte)-1),
// (*) new MoyoungSetting("WATCH_FACE_LAYOUT", MoyoungConstants.CMD_QUERY_WATCH_FACE_LAYOUT, MoyoungConstants.CMD_SET_WATCH_FACE_LAYOUT),
new MoyoungSettingByte("DISPLAY_WATCH_FACE", MoyoungConstants.CMD_QUERY_DISPLAY_WATCH_FACE, MoyoungConstants.CMD_SET_DISPLAY_WATCH_FACE),
new MoyoungSettingBool("OTHER_MESSAGE_STATE", MoyoungConstants.CMD_QUERY_OTHER_MESSAGE_STATE, MoyoungConstants.CMD_SET_OTHER_MESSAGE_STATE),
new MoyoungSettingBool("QUICK_VIEW", MoyoungConstants.CMD_QUERY_QUICK_VIEW, MoyoungConstants.CMD_SET_QUICK_VIEW),
new MoyoungSettingTimeRange("QUICK_VIEW_TIME", MoyoungConstants.CMD_QUERY_QUICK_VIEW_TIME, MoyoungConstants.CMD_SET_QUICK_VIEW_TIME),
new MoyoungSettingBool("SEDENTARY_REMINDER", MoyoungConstants.CMD_QUERY_SEDENTARY_REMINDER, MoyoungConstants.CMD_SET_SEDENTARY_REMINDER),
new MoyoungSettingRemindersToMove("REMINDERS_TO_MOVE_PERIOD", MoyoungConstants.CMD_QUERY_REMINDERS_TO_MOVE_PERIOD, MoyoungConstants.CMD_SET_REMINDERS_TO_MOVE_PERIOD),
new MoyoungSettingTimeRange("DO_NOT_DISTURB_TIME", MoyoungConstants.CMD_QUERY_DO_NOT_DISTURB_TIME, MoyoungConstants.CMD_SET_DO_NOT_DISTURB_TIME),
new MoyoungSettingBool("DO_NOT_DISTURB_ONOFF", MoyoungConstants.CMD_QUERY_DO_NOT_DISTURB_TIME, MoyoungConstants.CMD_SET_DO_NOT_DISTURB_TIME),
// (*) new MoyoungSetting("PSYCHOLOGICAL_PERIOD", MoyoungConstants.CMD_QUERY_PSYCHOLOGICAL_PERIOD, MoyoungConstants.CMD_SET_PSYCHOLOGICAL_PERIOD),
new MoyoungSettingBool("BREATHING_LIGHT", MoyoungConstants.CMD_QUERY_BREATHING_LIGHT, MoyoungConstants.CMD_SET_BREATHING_LIGHT),
new MoyoungSettingBool("POWER_SAVING", MoyoungConstants.CMD_QUERY_POWER_SAVING, MoyoungConstants.CMD_SET_POWER_SAVING)
};
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
final List<Integer> generic = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.GENERIC);
generic.add(R.xml.devicesettings_moyoung_device_version);
generic.add(R.xml.devicesettings_timeformat);
generic.add(R.xml.devicesettings_moyoung_watchface);
generic.add(R.xml.devicesettings_power_saving);
generic.add(R.xml.devicesettings_liftwrist_display);
// generic.add(R.xml.devicesettings_donotdisturb_no_auto); // not supported by Colmi i28 Ultra
generic.add(R.xml.devicesettings_donotdisturb_on_off_follow);
generic.add(R.xml.devicesettings_world_clocks);
generic.add(R.xml.devicesettings_sync_calendar);
final List<Integer> health = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.HEALTH);
health.add(R.xml.devicesettings_heartrate_interval);
health.add(R.xml.devicesettings_inactivity_with_steps);
return deviceSpecificSettings;
}
@Override
public String[] getSupportedLanguageSettings(final GBDevice device) {
// TODO: use settings customizer to display the languages
// retrieved from the watch instead of this fixed list
return new String[]{
"ar_SA",
"cs_CZ",
"de_DE",
"en_US",
"es_ES",
"fr_FR",
"it_IT",
"ja_JP",
"ko_KO",
"nl_NL",
"pl_PL",
"pt_PT",
"ro_RO",
"ru_RU",
"uk_UA",
"zh_CN",
};
};
@Override
public List<HeartRateCapability.MeasurementInterval> getHeartRateMeasurementIntervals() {
return Arrays.asList(
HeartRateCapability.MeasurementInterval.OFF,
HeartRateCapability.MeasurementInterval.MINUTES_5,
HeartRateCapability.MeasurementInterval.MINUTES_10,
HeartRateCapability.MeasurementInterval.MINUTES_20,
HeartRateCapability.MeasurementInterval.MINUTES_30
);
}
public MoyoungSetting[] getSupportedSettings() {
return MOYOUNG_SETTINGS;
}
public int getMtu() {
return 20;
}
}

View File

@ -0,0 +1,83 @@
/* Copyright (C) 2024 Arjan Schrijver
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung;
import androidx.annotation.DrawableRes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class ColmiI28UltraCoordinator extends AbstractMoyoungDeviceCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(ColmiI28UltraCoordinator.class);
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("i28 Ultra");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_colmi_i28_ultra;
}
@Override
@DrawableRes
public int getDefaultIconResource() {
return R.drawable.ic_device_miwatch;
}
@Override
@DrawableRes
public int getDisabledIconResource() {
return R.drawable.ic_device_miwatch_disabled;
}
@Override
public String getManufacturer() {
return "Colmi";
}
@Override
public int getMtu() {
return 508;
}
@Override
public int getAlarmSlotCount(GBDevice device) {
return 8;
}
@Override
public int getWorldClocksSlotCount() {
return 6;
}
@Override
public int getWorldClocksLabelLength() {
return 30;
}
@Override
public boolean supportsRemSleep() {
return true;
}
}

View File

@ -0,0 +1,63 @@
/* Copyright (C) 2024 Arjan Schrijver
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung;
import androidx.annotation.DrawableRes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
public class MisirunC17Coordinator extends AbstractMoyoungDeviceCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(MisirunC17Coordinator.class);
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("C17");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_misirun_c17;
}
@Override
@DrawableRes
public int getDefaultIconResource() {
return R.drawable.ic_device_banglejs;
}
@Override
@DrawableRes
public int getDisabledIconResource() {
return R.drawable.ic_device_banglejs_disabled;
}
@Override
public String getManufacturer() {
return "Misirun";
}
@Override
public int getMtu() {
return 508;
}
}

View File

@ -0,0 +1,436 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
public class MoyoungConstants {
// (*) - based only on static reverse engineering of the original app code,
// not supported by my watch so not implemented
// (or at least I didn't manage to get any response out of it)
// (?) - not checked
// The device communicates by sending packets by writing to UUID_CHARACTERISTIC_DATA_OUT
// in MTU-sized chunks. The value of MTU seems to be somehow changeable (?), but the default
// is 20. Responses are received via notify on UUID_CHARACTERISTIC_DATA_IN in similar format.
// The write success notification comes AFTER the responses.
// Packet format:
// packet[0] = 0xFE;
// packet[1] = 0xEA;
// if (MTU == 20) // could be a protocol version check?
// {
// packet[2] = 16;
// packet[3] = packet.length;
// }
// else
// {
// packet[2] = 32 + (packet.length >> 8) & 0xFF;
// packet[3] = packet.length & 0xFF;
// }
// packet[4] = packetType;
// packet[5:] = payload;
// Protocol version is determined by reading manufacturer name. MOYOUNG for old fixed-size
// or MOYOUNG-V2 for MTU. The non-MTU version uses packets of size 256
// for firmware >= 1.6.5, and 64 otherwise.
// The firmware version is also used to detect availability of some features.
// Additionally, there seems to be a trace of special packets with cmd 1 and 2, that are sent
// to UUID_CHARACTERISTIC_DATA_SPECIAL_1 and UUID_CHARACTERISTIC_DATA_SPECIAL_2 instead.
// They don't appear on my watch though.
// The response to CMD_ECG is special and is returned using UUID_CHARACTERISTIC_DATA_ECG_OLD
// or UUID_CHARACTERISTIC_DATA_ECG_NEW. The old version is clearly labeled as old in the
// unobfuscated part of the code. If both of them exist, old is used (but I presume only one
// of them is supposed to exist at a time). They also don't appear on my watch as it doesn't
// support ECG.
// In addition to the proprietary protocol described above, the following standard BLE services
// are used:
// * org.bluetooth.service.generic_access for device name
// * org.bluetooth.service.device_information for manufacturer, model, serial number and
// firmware version
// * org.bluetooth.service.battery_service for battery level
// * org.bluetooth.service.heart_rate is exposed, but doesn't seem to work
// * org.bluetooth.service.human_interface_device is exposed, but not even mentioned
// in the official app (?) - needs further research
// * the custom UUID_CHARACTERISTIC_STEPS is used to sync the pedometer data in real time
// via READ or NOTIFY - it's identical to the "sync past data" packet
// ({distance:uint24, steps:uint24, calories:uint24})
// * (?) 0000FEE7-0000-1000-8000-00805F9B34FB another custom service
// (NOT UUID_CHARACTERISTIC_DATA_ECG_OLD!!!) not mentioned anywhere in the official app,
// containing the following characteristics:
// * 0000FEA1-0000-1000-8000-00805F9B34FB - READ, NOTIFY
// * 0000FEC9-0000-1000-8000-00805F9B34FB - READ
// The above standard services are internally handled by the app using the following
// "packet numbers":
// * 16 - query steps
// * 17 - firmware version
// * 18 - query battery
// * 19 - DFU status (queries model number, looks for the string DFU and a number == 0 or != 0)
// * 20 - protocol version (queries manufacturer name, see description above)
public static final UUID UUID_SERVICE_MOYOUNG = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "feea"));
public static final UUID UUID_CHARACTERISTIC_STEPS = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee1"));
public static final UUID UUID_CHARACTERISTIC_DATA_OUT = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee2"));
public static final UUID UUID_CHARACTERISTIC_DATA_IN = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee3"));
public static final UUID UUID_CHARACTERISTIC_DATA_SPECIAL_1 = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee5")); // (*)
public static final UUID UUID_CHARACTERISTIC_DATA_SPECIAL_2 = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee6")); // (*)
public static final UUID UUID_CHARACTERISTIC_DATA_ECG_OLD = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee7")); // (*)
public static final UUID UUID_CHARACTERISTIC_DATA_ECG_NEW = UUID.fromString(String.format(AbstractBTLEDeviceSupport.BASE_UUID, "fee8")); // (*)
// Special
public static final byte CMD_SHUTDOWN = 81; // {-1}
public static final byte CMD_FIND_MY_WATCH = 97; // {}
public static final byte CMD_FIND_MY_PHONE = 98; // (*) outgoing {-1} to stop, incoming {0} start, {!=0} stop
public static final byte CMD_HS_DFU = 99; // (?) {1} - enableHsDfu(), {0} - queryHsDfuAddress()
// Activity/training tracking
// CMD_QUERY_LAST_DYNAMIC_RATE is triggered immediately after a training recording is finished on the watch.
// The watch sends CMD_QUERY_LAST_DYNAMIC_RATE command to the phone with the first part of the data, and then
// the phone is supposed to respond with empty CMD_QUERY_LAST_DYNAMIC_RATE to retrieve the next part.
// There seems to be no way to query this data later, or to start communication from phone side.
// The data format is uint32 date_recorded, uint8 heart_rate[] (where 0 is invalid measurement and
// data is recorded every 1 minute)
// CMD_QUERY_MOVEMENT_HEART_RATE returns the summary of last 3 trainings recorded on the watch.
// This is a cyclic buffer, so the watch will first overwrite entry number 0, then 1, then 2, then 0 again
// CMD_QUERY_PAST_HEART_RATE_1 and CMD_QUERY_PAST_HEART_RATE_2 don't seem to work at all on my watch.
// All "date recorded" values are in the hardcoded GMT+8 watch timezone
public static final byte CMD_QUERY_LAST_DYNAMIC_RATE = 52; // TRANSMISSION TRIGGERED FROM WATCH SIDE AFTER FINISHED TRAINING. Does custom packet splitting. The packet takes no data as input. Send the query repeatedly until you get all the data. THE FIRST PACKET IS SENT BY THE WATCH - THE PHONE QUERIES THIS COMMAND TO GET THE NEXT PART. The response starts with one byte: 0 for first packet, 1 for continuation packet, 2 for end of data. 0,time:uint32,measurement:uint8[] 1,measurement:uint8[] 1,measurement:uint8[] 2
public static final byte CMD_QUERY_PAST_HEART_RATE_1 = 53; // (*) Two arrays built of 4 packets each. See below. todayHeartRate(1) starts at 0 and ends at 3, yesterdayHeartRate() starts at 4 and ends at 7. Sampled every 5 minutes.
public static final byte CMD_QUERY_PAST_HEART_RATE_2 = 54; // (*) An array built of 20 packets. The packet takes the index as input. i.e. {x} -> {data[N*x], data[N*x+1], ..., data[N*x+N-1]} for x in 0-19 -- todayHeartRate(2). Sampled every 1 minute.
public static final byte CMD_QUERY_MOVEMENT_HEART_RATE = 55; // {} -> One packet with 3 entries of 24 bytes each {startTime:uint32, endTime:uint32, validTime:uint16, entry_number:uint8, type:uint8, steps:uint32, distance:uint32, calories:uint16}, everything little endian
// first byte for CMD_QUERY_LAST_DYNAMIC_RATE packets
public static final byte ARG_TRANSMISSION_FIRST = 0;
public static final byte ARG_TRANSMISSION_NEXT = 1;
public static final byte ARG_TRANSMISSION_LAST = 2; // note: last packet always empty
// Health measurements
public static final byte CMD_QUERY_TIMING_MEASURE_HEART_RATE = 47; // (*) {} -> ???
public static final byte CMD_SET_TIMING_MEASURE_HEART_RATE = 31; // (*) {i}, i >= 0, 0 is disabled
public static final byte CMD_START_STOP_MEASURE_DYNAMIC_RATE = 104; // (*) {enabled ? 0 : -1}
public static final byte HR_INTERVAL_OFF = 0;
public static final byte HR_INTERVAL_5MIN = 1;
public static final byte HR_INTERVAL_10MIN = 2;
public static final byte HR_INTERVAL_20MIN = 4;
public static final byte HR_INTERVAL_30MIN = 6;
public static final byte CMD_TRIGGER_MEASURE_BLOOD_PRESSURE = 105; // (?) {0, 0, 0} to start, {-1, -1, -1} to stop -> {unused?, num1, num2}
public static final byte CMD_TRIGGER_MEASURE_BLOOD_OXYGEN = 107; // (?) {start ? 0 : -1} -> {num}
public static final byte CMD_TRIGGER_MEASURE_HEARTRATE = 109; // {start ? 0 : -1} -> {bpm}
public static final byte CMD_ECG = 111; // (?) {heart_rate} or {1} to start or {0} to stop or {2} to query
// ECG data is special and comes from UUID_CHARACTERISTIC_DATA_ECG_OLD or UUID_CHARACTERISTIC_DATA_ECG_NEW
// Functionality
public static final byte CMD_SYNC_TIME = 49; // {time >> 24, time >> 16, time >> 8, time, 8}, time is a timestamp in seconds in GMT+8
public static final byte CMD_SYNC_SLEEP = 50; // {} -> {type, start_h, start_m}, repeating, type is SOBER(0),LIGHT(1),RESTFUL(2)
public static final byte CMD_SYNC_PAST_SLEEP_AND_STEP = 51; // {b (see below)} -> {x<=2, distance:uint24, steps:uint24, calories:uint24} or {x>2, (sleep data like above)} - two functions same CMD
// NOTE: these names are as specified in the original app. They do NOT match what my watch actually does. See note in FetchDataOperation.
public static final byte ARG_SYNC_YESTERDAY_STEPS = 1;
public static final byte ARG_SYNC_DAY_BEFORE_YESTERDAY_STEPS = 2;
public static final byte ARG_SYNC_YESTERDAY_SLEEP = 3;
public static final byte ARG_SYNC_DAY_BEFORE_YESTERDAY_SLEEP = 4;
public static final byte SLEEP_SOBER = 0;
public static final byte SLEEP_LIGHT = 1;
public static final byte SLEEP_RESTFUL = 2;
public static final byte SLEEP_REM = 3;
public static final byte CMD_QUERY_SLEEP_ACTION = 58; // (*) {i} -> {hour, x[60]}
public static final byte CMD_SEND_MESSAGE = 65; // {type, message[]}, message is encoded with manual splitting by String.valueOf(0x2080)
// CMD_SEND_CALL_OFF_HOOK = 65; // {-1} - the same ID as above, different arguments
public static final byte CMD_SET_WEATHER_FUTURE = 66; // {weatherId, low_temp, high_temp} * 7
public static final byte CMD_SET_WEATHER_TODAY = 67; // {have_pm25 ? 1 : 0, weatherId, temp[, pm25 >> 8, pm25], lunar_or_festival[8], city[8]}, names are UTF-16BE encoded (4 characters each!)
public static final byte CMD_SET_WEATHER_LOCATION = 69; // {string utf8}
public static final byte CMD_SET_SUNRISE_SUNSET = -75; // {5 bytes unknown, sunrise hour, sunrise min, sunset hour, sunset min, string (location utf8)}
public static final byte CMD_SET_MUSIC_INFO = 68; // {artist=1/track=0, string}
public static final byte CMD_SET_MUSIC_STATE = 123; // {is_playing ? 1 : 0}
public static final byte CMD_GSENSOR_CALIBRATION = 82; // (?) {}
public static final byte CMD_QUERY_STEPS_CATEGORY = 89; // (*) {i} -> {0, data:uint16[*]}, {1}, {2, data:uint16[*]}, {3}, query 0+1 together and 2+3 together
//public static final byte ARG_QUERY_STEPS_CATEGORY_TODAY_STEPS = 0;
//public static final byte ARG_QUERY_STEPS_CATEGORY_YESTERDAY_STEPS = 2;
public static final byte CMD_SWITCH_CAMERA_VIEW = 102; // {} -> {}, outgoing open screen, incoming take photo
public static final byte CMD_NOTIFY_PHONE_OPERATION = 103; // ONLY INCOMING! -> {x}, x -> 0 = play/pause, 1 = prev, 2 = next, 3 = reject incoming call)
public static final byte CMD_NOTIFY_WEATHER_CHANGE = 100; // ONLY INCOMING! -> {} - when the watch really wants us to retransmit the weather again (it seems to often happen after stopping training - running the training blocks access to main menu so I guess it restarts afterwards or something). Will repeat whenever navigating the menu where the weather should be, and weather won't be visible on watch screen until that happens.
public static final byte ARG_OPERATION_PLAY_PAUSE = 0;
public static final byte ARG_OPERATION_PREV_SONG = 1;
public static final byte ARG_OPERATION_NEXT_SONG = 2;
public static final byte ARG_OPERATION_DROP_INCOMING_CALL = 3;
public static final byte ARG_OPERATION_VOLUME_UP = 4;
public static final byte ARG_OPERATION_VOLUME_DOWN = 5;
public static final byte ARG_OPERATION_PLAY = 6;
public static final byte ARG_OPERATION_PAUSE = 7;
public static final byte ARG_OPERATION_SEND_CURRENT_VOLUME = 12; // {0x00-0x10}
public static final byte CMD_QUERY_ALARM_CLOCK = 33; // (?) {} -> a list of entries like below
public static final byte CMD_SET_ALARM_CLOCK = 17; // (?) {id, enable ? 1 : 0, repeat, hour, minute, i >> 8, i, repeatMode}, repeatMode is 0(SINGLE), 127(EVERYDAY), or bitmask of 1,2,4,8,16,32,64(SUNDAY-SATURDAY) is 0,1,2, i is ((year << 12) + (month << 8) + day) where year is 2015-based, month and day start at 1 for repeatMode=SINGLE and 0 otherwise, repeat is 0(SINGLE),1(EVERYDAY),2(OTHER)
public static final byte CMD_ADVANCED_QUERY = (byte) 0xb9;
public static final byte CMD_ADVANCED_CMD = (byte) 0xbb;
public static final byte CMD_QUERY_POWER_SAVING = (byte) 0xa4;
public static final byte CMD_SET_POWER_SAVING = (byte) 0x94;
public static final byte ARG_ADVANCED_SET_ALARM = 0x05;
public static final byte ARG_ADVANCED_SET_CALENDAR = 0x08;
public static final byte ARG_ADVANCED_QUERY_STOCKS = 0x0e;
public static final byte ARG_ADVANCED_QUERY_ALARMS = 0x15;
public static final byte ARG_ALARM_SET = 0x00;
public static final byte ARG_ALARM_DELETE = 0x02;
public static final byte ARG_ALARM_FROM_WATCH = 0x04;
public static final byte ARG_CALENDAR_ADD_ITEM = 0x00;
public static final byte ARG_CALENDAR_DISABLE = 0x04;
public static final byte ARG_CALENDAR_FINISHED = 0x05;
public static final byte ARG_CALENDAR_CLEAR = 0x06;
public static final int MAX_CALENDAR_ITEMS = 12; // Tested only on Colmi i28 Ultra, move to coordinator if different on other devices
// Settings
public static final byte CMD_SET_USER_INFO = 18; // (?) {height, weight, age, gender}, MALE = 0, FEMALE = 1
public static final byte CMD_QUERY_DOMINANT_HAND = 36; // (*) {} -> {value}
public static final byte CMD_SET_DOMINANT_HAND = 20; // (*) {value}
public static final byte CMD_QUERY_DISPLAY_DEVICE_FUNCTION = 37; // (*) {} - current, {-1} - list all supported -> {[-1, ], ...} (prefixed with -1 if lists supported, nothing otherwise)
public static final byte CMD_SET_DISPLAY_DEVICE_FUNCTION = 21; // (*) {..., 0} - null terminated list of functions to enable
public static final byte CMD_QUERY_GOAL_STEP = 38; // {} -> {value, value >> 8, value >> 16, value >> 24} // this has the endianness swapped between query and set
public static final byte CMD_SET_GOAL_STEP = 22; // {value >> 24, value >> 16, value >> 8, value} // yes, really
public static final byte CMD_QUERY_TIME_SYSTEM = 39; // {} -> {value}
public static final byte CMD_SET_TIME_SYSTEM = 23; // {value}
// quick view = enable display when wrist is lifted
public static final byte CMD_QUERY_QUICK_VIEW = 40; // {} -> {value}
public static final byte CMD_SET_QUICK_VIEW = 24; // {enabled ? 1 : 0}
public static final byte CMD_QUERY_DISPLAY_WATCH_FACE = 41; // {} -> {value}
public static final byte CMD_SET_DISPLAY_WATCH_FACE = 25; // {value}
public static final byte CMD_QUERY_METRIC_SYSTEM = 42; // {} -> {value}
public static final byte CMD_SET_METRIC_SYSTEM = 26; // {value}
public static final byte CMD_QUERY_DEVICE_LANGUAGE = 43; // {} -> {value, bitmask_of_supported_langs:uint32}
public static final byte CMD_SET_DEVICE_LANGUAGE = 27; // {new_value}
// enables "other" (as in "not a messaging app") on the notifications configuration screen in the official app
// seems to be used only in the app, not sure why they even store it on the watch
public static final byte CMD_QUERY_OTHER_MESSAGE_STATE = 44; // {} -> {value}
public static final byte CMD_SET_OTHER_MESSAGE_STATE = 28; // {enabled ? 1 : 0}
public static final byte CMD_QUERY_SEDENTARY_REMINDER = 45; // {} -> {value}
public static final byte CMD_SET_SEDENTARY_REMINDER = 29; // {enabled ? 1 : 0}
public static final byte CMD_QUERY_DEVICE_VERSION = 46; // {} -> {value}
public static final byte CMD_SET_DEVICE_VERSION = 30; // {new_value}
public static final byte CMD_QUERY_WATCH_FACE_LAYOUT = 57; // (*) {} -> {time_position, time_top_content, time_bottom_content, text_color >> 8, text_color, background_picture_md5[32]}
public static final byte CMD_SET_WATCH_FACE_LAYOUT = 56; // (*) {time_position, time_top_content, time_bottom_content, text_color >> 8, text_color, background_picture_md5[32]}, text_color is R5G6B5, background_picture is stored as hex digits (numbers 0-15 not chars '0'-'F' !)
public static final byte CMD_SET_STEP_LENGTH = 84; // (?) {value}
public static final byte CMD_QUERY_DO_NOT_DISTURB_TIME = -127; // {} -> {start >> 8, start, end >> 8, end} these are 16-bit values (somebody was drunk while writing this or what?)
public static final byte CMD_SET_DO_NOT_DISTURB_TIME = 113; // {start_hour, start_min, end_hour, end_min}
public static final byte CMD_QUERY_QUICK_VIEW_TIME = -126; // {} -> {start >> 8, start, end >> 8, end} these are 16-bit values (somebody was drunk while writing this or what?)
public static final byte CMD_SET_QUICK_VIEW_TIME = 114; // {start_hour, start_min, end_hour, end_min}
public static final byte CMD_QUERY_REMINDERS_TO_MOVE_PERIOD = -125; // {} -> {period, steps, start_hour, end_hour}
public static final byte CMD_SET_REMINDERS_TO_MOVE_PERIOD = 115; // {period, steps, start_hour, end_hour}
public static final byte CMD_QUERY_SUPPORT_WATCH_FACE = -124; // (*) {} -> {count >> 8, count, ...}
public static final byte CMD_QUERY_PSYCHOLOGICAL_PERIOD = -123; // (*) {} -> ??? (too lazy to check, sorry :P)
public static final byte CMD_SET_PSYCHOLOGICAL_PERIOD = 117; // (*) {encodeConfiguredReminders(info), 15, info.getPhysiologcalPeriod(), info.getMenstrualPeriod(), info.startDate.get(Calendar.MONTH), info.startDate.get(Calendar.DATE), info.getReminderHour(), info.getReminderMinute(), info.getReminderHour(), info.getReminderMinute(), info.getReminderHour(), info.getReminderMinute(), info.getReminderHour(), info.getReminderMinute()}
// encodeConfiguredReminders(CRPPhysiologcalPeriodInfo info) {
// int i = info.isMenstrualReminder() ? 241 : 240;
// if (info.isOvulationReminder())
// i += 2;
// if (info.isOvulationDayReminder())
// i += 4;
// if (info.isOvulationEndReminder())
// i += 8;
// return (byte) i;
// }
// no idea what this does
public static final byte CMD_QUERY_BREATHING_LIGHT = -120; // {} -> {value}
public static final byte CMD_SET_BREATHING_LIGHT = 120; // {enabled ? 1 : 0}
public static final byte TRAINING_TYPE_WALK = 0;
public static final byte TRAINING_TYPE_RUN = 1;
public static final byte TRAINING_TYPE_BIKING = 2;
public static final byte TRAINING_TYPE_ROPE = 3;
public static final byte TRAINING_TYPE_BADMINTON = 4;
public static final byte TRAINING_TYPE_BASKETBALL = 5;
public static final byte TRAINING_TYPE_FOOTBALL = 6;
public static final byte TRAINING_TYPE_SWIM = 7;
public static final byte TRAINING_TYPE_MOUNTAINEERING = 8;
public static final byte TRAINING_TYPE_TENNIS = 9;
public static final byte TRAINING_TYPE_RUGBY = 10;
public static final byte TRAINING_TYPE_GOLF = 11;
// The watch stores all dates in GMT+8 time zone with seconds resolution
// These helper functions convert between the watch time representation and local system representation
public static int LocalTimeToWatchTime(Date localTime)
{
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
simpleDateFormat.setTimeZone(TimeZone.getDefault());
String format = simpleDateFormat.format(localTime);
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT+8"));
try {
return (int)(simpleDateFormat.parse(format).getTime() / 1000);
} catch (ParseException e) {
throw new IllegalArgumentException(e);
}
}
public static Date WatchTimeToLocalTime(int watchTime)
{
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT+8"));
String format = simpleDateFormat.format(new Date((long)watchTime * 1000));
simpleDateFormat.setTimeZone(TimeZone.getDefault());
try {
return simpleDateFormat.parse(format);
} catch (ParseException e) {
throw new IllegalArgumentException(e);
}
}
// The notification types used by CMD_SEND_MESSAGE
public static final byte NOTIFICATION_TYPE_CALL_OFF_HOOK = -1;
public static final byte NOTIFICATION_TYPE_CALL = 0;
public static final byte NOTIFICATION_TYPE_MESSAGE_SMS = 1;
public static final byte NOTIFICATION_TYPE_MESSAGE_WECHAT = 2;
public static final byte NOTIFICATION_TYPE_MESSAGE_QQ = 3;
public static final byte NOTIFICATION_TYPE_MESSAGE_FACEBOOK = 4;
public static final byte NOTIFICATION_TYPE_MESSAGE_TWITTER = 5;
public static final byte NOTIFICATION_TYPE_MESSAGE_INSTAGRAM = 6;
public static final byte NOTIFICATION_TYPE_MESSAGE_SKYPE = 7;
public static final byte NOTIFICATION_TYPE_MESSAGE_WHATSAPP = 8;
public static final byte NOTIFICATION_TYPE_MESSAGE_LINE = 9;
public static final byte NOTIFICATION_TYPE_MESSAGE_KAKAO = 10;
public static final byte NOTIFICATION_TYPE_MESSAGE_OTHER = 11;
public static byte notificationType(NotificationType type)
{
switch(type)
{
case FACEBOOK:
case FACEBOOK_MESSENGER:
return NOTIFICATION_TYPE_MESSAGE_FACEBOOK;
case GENERIC_SMS:
return NOTIFICATION_TYPE_MESSAGE_SMS;
case INSTAGRAM:
return NOTIFICATION_TYPE_MESSAGE_INSTAGRAM;
case KAKAO_TALK:
return NOTIFICATION_TYPE_MESSAGE_KAKAO;
case LINE:
return NOTIFICATION_TYPE_MESSAGE_LINE;
case SKYPE:
return NOTIFICATION_TYPE_MESSAGE_SKYPE;
case TWITTER:
return NOTIFICATION_TYPE_MESSAGE_TWITTER;
case WECHAT:
return NOTIFICATION_TYPE_MESSAGE_WECHAT;
case WHATSAPP:
return NOTIFICATION_TYPE_MESSAGE_WHATSAPP;
default:
return NOTIFICATION_TYPE_MESSAGE_OTHER;
}
}
// Weather types
public static final byte WEATHER_CLOUDY = 0;
public static final byte WEATHER_FOGGY = 1;
public static final byte WEATHER_OVERCAST = 2;
public static final byte WEATHER_RAINY = 3;
public static final byte WEATHER_SNOWY = 4;
public static final byte WEATHER_SUNNY = 5;
public static final byte WEATHER_SANDSTORM = 6; // aka "wind", according to the image
public static final byte WEATHER_HAZE = 7; // it's basically very big fog :P
// NOTE: values > 7 give random glitchy crap as images :D
public static byte openWeatherConditionToMoyoungConditionId(int openWeatherMapCondition) {
int openWeatherMapGroup = openWeatherMapCondition / 100;
switch (openWeatherMapGroup) {
case 2: // thunderstorm
case 3: // drizzle
case 5: // rain
return MoyoungConstants.WEATHER_RAINY;
case 6: // snow
return MoyoungConstants.WEATHER_SNOWY;
case 7: // fog
return MoyoungConstants.WEATHER_FOGGY;
case 8: // clear / clouds
if (openWeatherMapCondition <= 801) // few clouds
return MoyoungConstants.WEATHER_SUNNY;
if (openWeatherMapCondition >= 804) // overcast clouds
return MoyoungConstants.WEATHER_CLOUDY;
return MoyoungConstants.WEATHER_OVERCAST;
case 9: // extreme
default:
if (openWeatherMapCondition == 905) // windy
return MoyoungConstants.WEATHER_SANDSTORM;
return MoyoungConstants.WEATHER_HAZE;
}
}
public static final String PREF_MOYOUNG_WATCH_FACE = "moyoung_watch_face";
public static final String PREF_LANGUAGE = "moyoung_language";
public static final String PREF_LANGUAGE_SUPPORT = "moyoung_language_supported";
public static final String PREF_MOYOUNG_DEVICE_VERSION = "moyoung_device_version";
}

View File

@ -0,0 +1,38 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
public class MoyoungWeatherForecast {
public final byte conditionId;
public final byte minTemp;
public final byte maxTemp;
public MoyoungWeatherForecast(byte conditionId, byte minTemp, byte maxTemp) {
this.conditionId = conditionId;
this.minTemp = minTemp;
this.maxTemp = maxTemp;
}
public MoyoungWeatherForecast(WeatherSpec.Daily forecast)
{
conditionId = MoyoungConstants.openWeatherConditionToMoyoungConditionId(forecast.conditionCode);
minTemp = (byte)(forecast.minTemp - 273); // Kelvin -> Celcius
maxTemp = (byte)(forecast.maxTemp - 273); // Kelvin -> Celcius
}
}

View File

@ -0,0 +1,52 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class MoyoungWeatherToday {
public final byte conditionId;
public final byte currentTemp;
public final Short pm25; // (*)
public final String lunar_or_festival; // (*)
public final String city; // (*)
public MoyoungWeatherToday(byte conditionId, byte currentTemp, @Nullable Short pm25, @NonNull String lunar_or_festival, @NonNull String city) {
if (lunar_or_festival.length() != 4)
throw new IllegalArgumentException("lunar_or_festival");
if (city.length() != 4)
throw new IllegalArgumentException("city");
this.conditionId = conditionId;
this.currentTemp = currentTemp;
this.pm25 = pm25;
this.lunar_or_festival = lunar_or_festival;
this.city = city;
}
public MoyoungWeatherToday(WeatherSpec weatherSpec)
{
conditionId = MoyoungConstants.openWeatherConditionToMoyoungConditionId(weatherSpec.currentConditionCode);
currentTemp = (byte)(weatherSpec.currentTemp - 273); // Kelvin -> Celcius
pm25 = null;
lunar_or_festival = StringUtils.pad("", 4);
city = StringUtils.pad(weatherSpec.location.substring(0, 4), 4);
}
}

View File

@ -0,0 +1,342 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.internal.SqlUtils;
import de.greenrobot.dao.query.WhereCondition;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.MoyoungConstants;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungHeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class MoyoungActivitySampleProvider extends AbstractSampleProvider<MoyoungActivitySample> {
private static final Logger LOG = LoggerFactory.getLogger(MoyoungActivitySampleProvider.class);
public static final int SOURCE_NOT_MEASURED = -1;
public static final int SOURCE_STEPS_REALTIME = 1; // steps gathered at realtime from the steps characteristic
public static final int SOURCE_STEPS_SUMMARY = 2; // steps gathered from the daily summary
public static final int SOURCE_STEPS_IDLE = 3; // idle sample inserted because the user was not moving (to differentiate from missing data because watch not connected)
public static final int SOURCE_SLEEP_SUMMARY = 4; // data collected from the sleep function
public static final int SOURCE_SINGLE_MEASURE = 5; // heart rate / blood data gathered from the "single measurement" function
public static final int SOURCE_TRAINING_HEARTRATE = 6; // heart rate data collected from the training function
public static final int SOURCE_BATTERY = 7; // battery report
public static final int ACTIVITY_NOT_MEASURED = -1;
public static final int ACTIVITY_TRAINING_WALK = MoyoungConstants.TRAINING_TYPE_WALK;
public static final int ACTIVITY_TRAINING_RUN = MoyoungConstants.TRAINING_TYPE_RUN;
public static final int ACTIVITY_TRAINING_BIKING = MoyoungConstants.TRAINING_TYPE_BIKING;
public static final int ACTIVITY_TRAINING_ROPE = MoyoungConstants.TRAINING_TYPE_ROPE;
public static final int ACTIVITY_TRAINING_BADMINTON = MoyoungConstants.TRAINING_TYPE_BADMINTON;
public static final int ACTIVITY_TRAINING_BASKETBALL = MoyoungConstants.TRAINING_TYPE_BASKETBALL;
public static final int ACTIVITY_TRAINING_FOOTBALL = MoyoungConstants.TRAINING_TYPE_FOOTBALL;
public static final int ACTIVITY_TRAINING_SWIM = MoyoungConstants.TRAINING_TYPE_SWIM;
public static final int ACTIVITY_TRAINING_MOUNTAINEERING = MoyoungConstants.TRAINING_TYPE_MOUNTAINEERING;
public static final int ACTIVITY_TRAINING_TENNIS = MoyoungConstants.TRAINING_TYPE_TENNIS;
public static final int ACTIVITY_TRAINING_RUGBY = MoyoungConstants.TRAINING_TYPE_RUGBY;
public static final int ACTIVITY_TRAINING_GOLF = MoyoungConstants.TRAINING_TYPE_GOLF;
public static final int ACTIVITY_SLEEP_LIGHT = 16;
public static final int ACTIVITY_SLEEP_RESTFUL = 17;
public static final int ACTIVITY_SLEEP_START = 18;
public static final int ACTIVITY_SLEEP_END = 19;
public static final int ACTIVITY_SLEEP_REM = 20;
public MoyoungActivitySampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public AbstractDao<MoyoungActivitySample, ?> getSampleDao() {
return getSession().getMoyoungActivitySampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return MoyoungActivitySampleDao.Properties.Timestamp;
}
@Nullable
@Override
protected Property getRawKindSampleProperty() {
return MoyoungActivitySampleDao.Properties.RawKind;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return MoyoungActivitySampleDao.Properties.DeviceId;
}
@Override
public MoyoungActivitySample createActivitySample() {
return new MoyoungActivitySample();
}
@Override
public ActivityKind normalizeType(int rawType) {
if (rawType == ACTIVITY_NOT_MEASURED)
return ActivityKind.NOT_MEASURED;
else if (rawType == ACTIVITY_SLEEP_LIGHT)
return ActivityKind.LIGHT_SLEEP;
else if (rawType == ACTIVITY_SLEEP_RESTFUL)
return ActivityKind.DEEP_SLEEP;
else if (rawType == ACTIVITY_SLEEP_REM)
return ActivityKind.REM_SLEEP;
else if (rawType == ACTIVITY_SLEEP_START || rawType == ACTIVITY_SLEEP_END)
return ActivityKind.NOT_MEASURED;
else if (rawType == ACTIVITY_TRAINING_WALK)
return ActivityKind.WALKING;
else if (rawType == ACTIVITY_TRAINING_RUN)
return ActivityKind.RUNNING;
else if (rawType == ACTIVITY_TRAINING_BIKING)
return ActivityKind.CYCLING;
else if (rawType == ACTIVITY_TRAINING_SWIM)
return ActivityKind.SWIMMING;
else if (rawType == ACTIVITY_TRAINING_ROPE || rawType == ACTIVITY_TRAINING_BADMINTON ||
rawType == ACTIVITY_TRAINING_BASKETBALL || rawType == ACTIVITY_TRAINING_FOOTBALL ||
rawType == ACTIVITY_TRAINING_MOUNTAINEERING || rawType == ACTIVITY_TRAINING_TENNIS ||
rawType == ACTIVITY_TRAINING_RUGBY || rawType == ACTIVITY_TRAINING_GOLF)
return ActivityKind.EXERCISE;
else
return ActivityKind.ACTIVITY;
}
@Override
public int toRawActivityKind(ActivityKind activityKind) {
if (activityKind == ActivityKind.NOT_MEASURED)
return ACTIVITY_NOT_MEASURED;
else if (activityKind == ActivityKind.LIGHT_SLEEP)
return ACTIVITY_SLEEP_LIGHT;
else if (activityKind == ActivityKind.DEEP_SLEEP)
return ACTIVITY_SLEEP_RESTFUL;
else if (activityKind == ActivityKind.REM_SLEEP)
return ACTIVITY_SLEEP_REM;
else if (activityKind == ActivityKind.ACTIVITY)
return ACTIVITY_NOT_MEASURED; // TODO: ?
else
throw new IllegalArgumentException("Invalid Gadgetbridge activity kind: " + activityKind);
}
final ActivityKind sleepStageToActivityKind(final int sleepStage) {
switch (sleepStage) {
case MoyoungConstants.SLEEP_LIGHT:
return ActivityKind.LIGHT_SLEEP;
case MoyoungConstants.SLEEP_RESTFUL:
return ActivityKind.DEEP_SLEEP;
case MoyoungConstants.SLEEP_REM:
return ActivityKind.REM_SLEEP;
case MoyoungConstants.SLEEP_SOBER:
return ActivityKind.AWAKE_SLEEP;
default:
return ActivityKind.UNKNOWN;
}
}
@Override
public float normalizeIntensity(int rawIntensity) {
if (rawIntensity == ActivitySample.NOT_MEASURED)
return Float.NEGATIVE_INFINITY;
else
return rawIntensity;
}
@Override
protected List<MoyoungActivitySample> getGBActivitySamples(final int timestamp_from, final int timestamp_to) {
LOG.trace(
"Getting Moyoung activity samples between {} and {}",
timestamp_from,
timestamp_to
);
final long nanoStart = System.nanoTime();
final List<MoyoungActivitySample> samples = fillGaps(
super.getGBActivitySamples(timestamp_from, timestamp_to),
timestamp_from,
timestamp_to
);
final Map<Integer, MoyoungActivitySample> sampleByTs = new HashMap<>();
for (final MoyoungActivitySample sample : samples) {
sampleByTs.put(sample.getTimestamp(), sample);
}
overlayHeartRate(sampleByTs, timestamp_from, timestamp_to);
overlaySleep(sampleByTs, timestamp_from, timestamp_to);
final List<MoyoungActivitySample> finalSamples = new ArrayList<>(sampleByTs.values());
Collections.sort(finalSamples, Comparator.comparingInt(MoyoungActivitySample::getTimestamp));
final long nanoEnd = System.nanoTime();
final long executionTime = (nanoEnd - nanoStart) / 1000000;
LOG.trace("Getting Moyoung samples took {}ms", executionTime);
return finalSamples;
}
private void overlayHeartRate(final Map<Integer, MoyoungActivitySample> sampleByTs, final int timestamp_from, final int timestamp_to) {
final MoyoungHeartRateSampleProvider heartRateSampleProvider = new MoyoungHeartRateSampleProvider(getDevice(), getSession());
final List<MoyoungHeartRateSample> hrSamples = heartRateSampleProvider.getAllSamples(timestamp_from * 1000L, timestamp_to * 1000L);
for (final MoyoungHeartRateSample hrSample : hrSamples) {
// round to the nearest minute, we don't need per-second granularity
final int tsSeconds = (int) ((hrSample.getTimestamp() / 1000) / 60) * 60;
MoyoungActivitySample sample = sampleByTs.get(tsSeconds);
if (sample == null) {
sample = new MoyoungActivitySample();
sample.setTimestamp(tsSeconds);
sample.setProvider(this);
sampleByTs.put(tsSeconds, sample);
}
sample.setHeartRate(hrSample.getHeartRate());
}
}
private void overlaySleep(final Map<Integer, MoyoungActivitySample> sampleByTs, final int timestamp_from, final int timestamp_to) {
final MoyoungSleepStageSampleProvider sleepStageSampleProvider = new MoyoungSleepStageSampleProvider(getDevice(), getSession());
final List<MoyoungSleepStageSample> sleepStageSamples = sleepStageSampleProvider.getAllSamples(timestamp_from * 1000L, timestamp_to * 1000L);
// Retrieve the last stage before this time range, as the user could have been asleep during
// the range transition
final MoyoungSleepStageSample lastSleepStageBeforeRange = sleepStageSampleProvider.getLastSampleBefore(timestamp_from * 1000L);
if (lastSleepStageBeforeRange != null && lastSleepStageBeforeRange.getStage() != MoyoungConstants.SLEEP_SOBER) {
LOG.debug("Last sleep stage before range: ts={}, stage={}", lastSleepStageBeforeRange.getTimestamp(), lastSleepStageBeforeRange.getStage());
sleepStageSamples.add(0, lastSleepStageBeforeRange);
}
// Retrieve the next sample after the time range, as the last stage could exceed it
final MoyoungSleepStageSample nextSleepStageAfterRange = sleepStageSampleProvider.getNextSampleAfter(timestamp_to * 1000L);
if (nextSleepStageAfterRange != null) {
LOG.debug("Next sleep stage after range: ts={}, stage={}", nextSleepStageAfterRange.getTimestamp(), nextSleepStageAfterRange.getStage());
sleepStageSamples.add(nextSleepStageAfterRange);
}
if (sleepStageSamples.size() > 1) {
LOG.debug("Overlaying with data from {} sleep stage samples", sleepStageSamples.size());
} else {
LOG.warn("Not overlaying sleep data because more than 1 sleep stage sample is required");
return;
}
MoyoungSleepStageSample prevSample = null;
for (final MoyoungSleepStageSample sleepStageSample : sleepStageSamples) {
if (prevSample == null) {
prevSample = sleepStageSample;
continue;
}
final ActivityKind sleepRawKind = sleepStageToActivityKind(prevSample.getStage());
if (sleepRawKind.equals(ActivityKind.AWAKE_SLEEP)) {
prevSample = sleepStageSample;
continue;
}
// round to the nearest minute, we don't need per-second granularity
final int tsSecondsPrev = (int) ((prevSample.getTimestamp() / 1000) / 60) * 60;
final int tsSecondsCur = (int) ((sleepStageSample.getTimestamp() / 1000) / 60) * 60;
for (int i = tsSecondsPrev; i < tsSecondsCur; i += 60) {
if (i < timestamp_from || i > timestamp_to) continue;
MoyoungActivitySample sample = sampleByTs.get(i);
if (sample == null) {
sample = new MoyoungActivitySample();
sample.setTimestamp(i);
sample.setProvider(this);
sampleByTs.put(i, sample);
}
sample.setRawKind(toRawActivityKind(sleepRawKind));
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
}
prevSample = sleepStageSample;
}
if (prevSample != null && !sleepStageToActivityKind(prevSample.getStage()).equals(ActivityKind.AWAKE_SLEEP)) {
LOG.warn("Last sleep stage sample was not of type awake");
}
}
/**
* Set the activity kind from NOT_MEASURED to new_raw_activity_kind on the given range
* @param timestamp_from the start timestamp
* @param timestamp_to the end timestamp
* @param new_raw_activity_kind the activity kind to set
*/
public void updateActivityInRange(int timestamp_from, int timestamp_to, int new_raw_activity_kind)
{
// greenDAO does not provide a bulk update functionality, and manual update fails because
// of no primary key
Property timestampProperty = getTimestampSampleProperty();
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null)
throw new IllegalStateException();
Property deviceProperty = getDeviceIdentifierSampleProperty();
/*QueryBuilder<MoyoungActivitySample> qb = getSampleDao().queryBuilder();
qb.where(deviceProperty.eq(dbDevice.getId()))
.where(timestampProperty.ge(timestamp_from), timestampProperty.le(timestamp_to))
.where(getRawKindSampleProperty().eq(ACTIVITY_NOT_MEASURED));
List<MoyoungActivitySample> samples = qb.build().list();
for (MoyoungActivitySample sample : samples) {
sample.setProvider(this);
sample.setRawKind(new_raw_activity_kind);
sample.update();
}*/
String tablename = getSampleDao().getTablename();
String baseSql = SqlUtils.createSqlUpdate(tablename, new String[] { getRawKindSampleProperty().columnName }, new String[] { });
StringBuilder builder = new StringBuilder(baseSql);
List<Object> values = new ArrayList<>();
values.add(new_raw_activity_kind);
List<WhereCondition> whereConditions = new ArrayList<>();
whereConditions.add(deviceProperty.eq(dbDevice.getId()));
whereConditions.add(timestampProperty.ge(timestamp_from));
whereConditions.add(timestampProperty.le(timestamp_to));
whereConditions.add(getRawKindSampleProperty().eq(ACTIVITY_NOT_MEASURED));
ListIterator<WhereCondition> iter = whereConditions.listIterator();
while (iter.hasNext()) {
if (iter.hasPrevious()) {
builder.append(" AND ");
}
WhereCondition condition = iter.next();
condition.appendTo(builder, tablename);
condition.appendValuesTo(values);
}
getSampleDao().getDatabase().execSQL(builder.toString(), values.toArray());
}
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2024 Arjan Schrijver
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples;
import androidx.annotation.NonNull;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungBloodPressureSample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungBloodPressureSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class MoyoungBloodPressureSampleProvider extends AbstractTimeSampleProvider<MoyoungBloodPressureSample> {
public MoyoungBloodPressureSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<MoyoungBloodPressureSample, ?> getSampleDao() {
return getSession().getMoyoungBloodPressureSampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return MoyoungBloodPressureSampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return MoyoungBloodPressureSampleDao.Properties.DeviceId;
}
@Override
public MoyoungBloodPressureSample createSample() {
return new MoyoungBloodPressureSample();
}
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2024 Arjan Schrijver
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples;
import androidx.annotation.NonNull;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungHeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungHeartRateSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class MoyoungHeartRateSampleProvider extends AbstractTimeSampleProvider<MoyoungHeartRateSample> {
public MoyoungHeartRateSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<MoyoungHeartRateSample, ?> getSampleDao() {
return getSession().getMoyoungHeartRateSampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return MoyoungHeartRateSampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return MoyoungHeartRateSampleDao.Properties.DeviceId;
}
@Override
public MoyoungHeartRateSample createSample() {
return new MoyoungHeartRateSample();
}
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2025 Arjan Schrijver
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples;
import androidx.annotation.NonNull;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSleepStageSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class MoyoungSleepStageSampleProvider extends AbstractTimeSampleProvider<MoyoungSleepStageSample> {
public MoyoungSleepStageSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<MoyoungSleepStageSample, ?> getSampleDao() {
return getSession().getMoyoungSleepStageSampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return MoyoungSleepStageSampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return MoyoungSleepStageSampleDao.Properties.DeviceId;
}
@Override
public MoyoungSleepStageSample createSample() {
return new MoyoungSleepStageSample();
}
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2024 Arjan Schrijver
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples;
import androidx.annotation.NonNull;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSpo2Sample;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSpo2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class MoyoungSpo2SampleProvider extends AbstractTimeSampleProvider<MoyoungSpo2Sample> {
public MoyoungSpo2SampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<MoyoungSpo2Sample, ?> getSampleDao() {
return getSession().getMoyoungSpo2SampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return MoyoungSpo2SampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return MoyoungSpo2SampleDao.Properties.DeviceId;
}
@Override
public MoyoungSpo2Sample createSample() {
return new MoyoungSpo2Sample();
}
}

View File

@ -0,0 +1,21 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
public interface MoyoungEnum {
byte value();
}

View File

@ -0,0 +1,33 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
public enum MoyoungEnumDeviceVersion implements MoyoungEnum {
CHINESE_EDITION((byte)0),
INTERNATIONAL_EDITION((byte)1);
public final byte value;
MoyoungEnumDeviceVersion(byte value) {
this.value = value;
}
@Override
public byte value() {
return value;
}
}

View File

@ -0,0 +1,33 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
public enum MoyoungEnumDominantHand implements MoyoungEnum {
LEFT_HAND((byte)0),
RIGHT_HAND((byte)1);
public final byte value;
MoyoungEnumDominantHand(byte value) {
this.value = value;
}
@Override
public byte value() {
return value;
}
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
public enum MoyoungEnumLanguage implements MoyoungEnum {
LANGUAGE_ENGLISH((byte)0),
LANGUAGE_CHINESE((byte)1),
LANGUAGE_JAPANESE((byte)2),
LANGUAGE_KOREAN((byte)3),
LANGUAGE_GERMAN((byte)4),
LANGUAGE_FRENCH((byte)5),
LANGUAGE_SPANISH((byte)6),
LANGUAGE_ARABIC((byte)7),
LANGUAGE_RUSSIAN((byte)8),
LANGUAGE_TRADITIONAL((byte)9),
LANGUAGE_UKRAINIAN((byte)10),
LANGUAGE_ITALIAN((byte)11),
LANGUAGE_PORTUGUESE((byte)12),
LANGUAGE_DUTCH((byte)13),
LANGUAGE_POLISH((byte)14),
LANGUAGE_SWEDISH((byte)15),
LANGUAGE_FINNISH((byte)16),
LANGUAGE_DANISH((byte)17),
LANGUAGE_NORWEGIAN((byte)18),
LANGUAGE_HUNGARIAN((byte)19),
LANGUAGE_CZECH((byte)20),
LANGUAGE_BULGARIAN((byte)21),
LANGUAGE_ROMANIAN((byte)22),
LANGUAGE_SLOVAK_LANGUAGE((byte)23),
LANGUAGE_LATVIAN((byte)24);
public final byte value;
MoyoungEnumLanguage(byte value) {
this.value = value;
}
@Override
public byte value() {
return value;
}
}

View File

@ -0,0 +1,33 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
public enum MoyoungEnumMetricSystem implements MoyoungEnum {
METRIC_SYSTEM((byte)0),
IMPERIAL_SYSTEM((byte)1);
public final byte value;
MoyoungEnumMetricSystem(byte value) {
this.value = value;
}
@Override
public byte value() {
return value;
}
}

View File

@ -0,0 +1,33 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
public enum MoyoungEnumTimeSystem implements MoyoungEnum {
TIME_SYSTEM_12((byte)0),
TIME_SYSTEM_24((byte)1);
public final byte value;
MoyoungEnumTimeSystem(byte value) {
this.value = value;
}
@Override
public byte value() {
return value;
}
}

View File

@ -0,0 +1,32 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
public abstract class MoyoungSetting<T> {
public final String name;
public final byte cmdQuery;
public final byte cmdSet;
public MoyoungSetting(String name, byte cmdQuery, byte cmdSet) {
this.name = name;
this.cmdQuery = cmdQuery;
this.cmdSet = cmdSet;
}
public abstract byte[] encode(T value);
public abstract T decode(byte[] data);
}

View File

@ -0,0 +1,37 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
public class MoyoungSettingBool extends MoyoungSetting<Boolean> {
public MoyoungSettingBool(String name, byte cmdQuery, byte cmdSet) {
super(name, cmdQuery, cmdSet);
}
@Override
public byte[] encode(Boolean value) {
return new byte[] { value ? (byte)1 : (byte)0 };
}
@Override
public Boolean decode(byte[] data) {
if (data.length != 1)
throw new IllegalArgumentException("Wrong data length, should be 1, was " + data.length);
if (data[0] != 0 && data[0] != 1)
throw new IllegalArgumentException("Expected a boolean, got " + data[0]);
return data[0] != 0;
}
}

View File

@ -0,0 +1,35 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
public class MoyoungSettingByte extends MoyoungSetting<Byte> {
public MoyoungSettingByte(String name, byte cmdQuery, byte cmdSet) {
super(name, cmdQuery, cmdSet);
}
@Override
public byte[] encode(Byte value) {
return new byte[] { value };
}
@Override
public Byte decode(byte[] data) {
if (data.length != 1)
throw new IllegalArgumentException("Wrong data length, should be 1, was " + data.length);
return data[0];
}
}

View File

@ -0,0 +1,54 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
public class MoyoungSettingEnum<T extends Enum <?> & MoyoungEnum> extends MoyoungSetting<T> {
protected final Class<T> clazz;
public MoyoungSettingEnum(String name, byte cmdQuery, byte cmdSet, Class<T> clazz) {
super(name, cmdQuery, cmdSet);
this.clazz = clazz;
}
public T findByValue(byte value)
{
for (T e : clazz.getEnumConstants()) {
if (e.value() == value) {
return e;
}
}
throw new IllegalArgumentException("No enum value for " + value);
}
@Override
public byte[] encode(T value) {
return new byte[] { value.value() };
}
@Override
public T decode(byte[] data) {
if (data.length < 1)
throw new IllegalArgumentException("Wrong data length, should be at least 1, was " + data.length);
return findByValue(data[0]);
}
public T[] decodeSupportedValues(byte[] data) {
return clazz.getEnumConstants();
}
}

View File

@ -0,0 +1,43 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class MoyoungSettingInt extends MoyoungSetting<Integer> {
public MoyoungSettingInt(String name, byte cmdQuery, byte cmdSet) {
super(name, cmdQuery, cmdSet);
}
@Override
public byte[] encode(Integer value) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.order(ByteOrder.BIG_ENDIAN); // <- this is what happens when somebody in China designs a communication protocol
buffer.putInt(value);
return buffer.array();
}
@Override
public Integer decode(byte[] data) {
if (data.length != 4)
throw new IllegalArgumentException("Wrong data length, should be 4, was " + data.length);
ByteBuffer buffer = ByteBuffer.wrap(data);
buffer.order(ByteOrder.LITTLE_ENDIAN); // <- yes, it's different here
return buffer.getInt();
}
}

View File

@ -0,0 +1,71 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
import android.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.devices.moyoung.QuerySettingsOperation;
public class MoyoungSettingLanguage extends MoyoungSettingEnum<MoyoungEnumLanguage> {
private static final Logger LOG = LoggerFactory.getLogger(MoyoungSettingLanguage.class);
public MoyoungSettingLanguage(String name, byte cmdQuery, byte cmdSet) {
super(name, cmdQuery, cmdSet, MoyoungEnumLanguage.class);
}
private Pair<MoyoungEnumLanguage, MoyoungEnumLanguage[]> decodeData(byte[] data) {
if (data.length < 5)
throw new IllegalArgumentException("Wrong data length, should be at least 5, was " + data.length);
byte[] current = new byte[] { data[0] };
byte[] supported = new byte[] { data[1], data[2], data[3], data[4] };
ByteBuffer buffer = ByteBuffer.wrap(supported);
int supportedNum = buffer.getInt();
String supportedStr = new StringBuffer(Integer.toBinaryString(supportedNum)).reverse().toString();
MoyoungEnumLanguage currentLanguage = super.decode(current);
List<MoyoungEnumLanguage> supportedLanguages = new ArrayList<>();
for (MoyoungEnumLanguage e : clazz.getEnumConstants()) {
if (e.value() >= supportedStr.length())
continue;
if (Integer.parseInt(supportedStr.substring(e.value(), e.value() + 1)) != 0)
supportedLanguages.add(e);
}
MoyoungEnumLanguage[] supportedLanguagesArr = new MoyoungEnumLanguage[supportedLanguages.size()];
LOG.debug("Supported languages: {}", supportedLanguages);
return Pair.create(currentLanguage, supportedLanguages.toArray(supportedLanguagesArr));
}
@Override
public MoyoungEnumLanguage decode(byte[] data) {
return decodeData(data).first;
}
@Override
public MoyoungEnumLanguage[] decodeSupportedValues(byte[] data) {
return decodeData(data).second;
}
}

View File

@ -0,0 +1,74 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
import java.nio.ByteBuffer;
public class MoyoungSettingRemindersToMove extends MoyoungSetting<MoyoungSettingRemindersToMove.RemindersToMove> {
public static class RemindersToMove {
public byte period;
public byte steps;
public byte start_h;
public byte end_h;
public RemindersToMove() {
}
public RemindersToMove(byte period, byte steps, byte start_h, byte end_h) {
this.period = period;
this.steps = steps;
this.start_h = start_h;
this.end_h = end_h;
}
@Override
public String toString() {
return "RemindersToMove{" +
"period=" + period +
", steps=" + steps +
", start_h=" + start_h +
", end_h=" + end_h +
'}';
}
}
public MoyoungSettingRemindersToMove(String name, byte cmdQuery, byte cmdSet) {
super(name, cmdQuery, cmdSet);
}
@Override
public byte[] encode(RemindersToMove value) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.put(value.period);
buffer.put(value.steps);
buffer.put(value.start_h);
buffer.put(value.end_h);
return buffer.array();
}
@Override
public RemindersToMove decode(byte[] data) {
if (data.length != 4)
throw new IllegalArgumentException("Wrong data length, should be 4, was " + data.length);
ByteBuffer buffer = ByteBuffer.wrap(data);
byte period = buffer.get();
byte steps = buffer.get();
byte start_h = buffer.get();
byte end_h = buffer.get();
return new RemindersToMove(period, steps, start_h, end_h);
}
}

View File

@ -0,0 +1,76 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class MoyoungSettingTimeRange extends MoyoungSetting<MoyoungSettingTimeRange.TimeRange> {
public static class TimeRange {
public byte start_h;
public byte start_m;
public byte end_h;
public byte end_m;
public TimeRange() {
}
public TimeRange(byte start_h, byte start_m, byte end_h, byte end_m) {
this.start_h = start_h;
this.start_m = start_m;
this.end_h = end_h;
this.end_m = end_m;
}
@Override
public String toString() {
return "TimeRange{" +
"start_h=" + start_h +
", start_m=" + start_m +
", end_h=" + end_h +
", end_m=" + end_m +
'}';
}
}
public MoyoungSettingTimeRange(String name, byte cmdQuery, byte cmdSet) {
super(name, cmdQuery, cmdSet);
}
// Yes, these are different. Was somebody drunk when designing this?
@Override
public byte[] encode(TimeRange value) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.put(value.start_h);
buffer.put(value.start_m);
buffer.put(value.end_h);
buffer.put(value.end_m);
return buffer.array();
}
@Override
public TimeRange decode(byte[] data) {
if (data.length != 4)
throw new IllegalArgumentException("Wrong data length, should be 4, was " + data.length);
ByteBuffer buffer = ByteBuffer.wrap(data);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short start = buffer.getShort();
short end = buffer.getShort();
return new TimeRange((byte)(start / 60), (byte)(start % 60), (byte)(end / 60), (byte)(start % 60));
}
}

View File

@ -0,0 +1,44 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings;
import org.apache.commons.lang3.NotImplementedException;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
public class MoyoungSettingUserInfo extends MoyoungSetting<ActivityUser> {
public MoyoungSettingUserInfo(String name, byte cmdSet) {
super(name, (byte)-1, cmdSet);
}
@Override
public byte[] encode(ActivityUser value) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.put((byte)value.getHeightCm());
buffer.put((byte)value.getWeightKg());
buffer.put((byte)value.getAge());
buffer.put((byte)value.getGender());
return buffer.array();
}
@Override
public ActivityUser decode(byte[] data) {
throw new NotImplementedException("decode");
}
}

View File

@ -0,0 +1,36 @@
/* Copyright (C) 2024 Arjan Schrijver
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.entities;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.model.BloodPressureSample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public abstract class AbstractBloodPressureSample extends AbstractTimeSample implements BloodPressureSample {
@NonNull
@Override
public String toString() {
return getClass().getSimpleName() + "{" +
"timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) +
", bpSystolic=" + getBpSystolic() +
", bpDiastolic=" + getBpDiastolic() +
", userId=" + getUserId() +
", deviceId=" + getDeviceId() +
"}";
}
}

View File

@ -35,6 +35,7 @@ import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
@ -54,7 +55,6 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -383,6 +383,12 @@ public class NotificationListener extends NotificationListenerService {
notificationSpec.type = AppNotificationType.getInstance().get(source);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationSpec.channelId = notification.getChannelId();
}
notificationSpec.category = notification.category;
//FIXME: some quirks lookup table would be the minor evil here
if (source.startsWith("com.fsck.k9")) {
if (NotificationCompat.isGroupSummary(notification)) {
@ -953,7 +959,9 @@ public class NotificationListener extends NotificationListenerService {
source.equals("com.android.mms") ||
source.equals("com.sonyericsson.conversations") ||
source.equals("com.android.messaging") ||
source.equals("org.smssecure.smssecure")) {
source.equals("org.smssecure.smssecure") ||
source.equals("org.fossify.messages") ||
source.equals("dev.octoshrimpy.quik")) {
if (!"never".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) {
LOG.info("Ignoring notification, it's an sms notification");
return true;

View File

@ -180,7 +180,9 @@ public class GBDeviceService implements DeviceService {
.putExtra(EXTRA_NOTIFICATION_SOURCEAPPID, notificationSpec.sourceAppId)
.putExtra(EXTRA_NOTIFICATION_ICONID, notificationSpec.iconId)
.putExtra(NOTIFICATION_PICTURE_PATH, notificationSpec.picturePath)
.putExtra(EXTRA_NOTIFICATION_DNDSUPPRESSED, notificationSpec.dndSuppressed);
.putExtra(EXTRA_NOTIFICATION_DNDSUPPRESSED, notificationSpec.dndSuppressed)
.putExtra(EXTRA_NOTIFICATION_CHANNEL_ID, notificationSpec.channelId)
.putExtra(EXTRA_NOTIFICATION_CATEGORY, notificationSpec.category);
invokeService(intent);
}

View File

@ -0,0 +1,22 @@
/* Copyright (C) 2024 Arjan Schrijver
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
public interface BloodPressureSample extends TimeSample {
int getBpSystolic();
int getBpDiastolic();
}

View File

@ -101,6 +101,8 @@ public interface DeviceService extends EventHandler {
String EXTRA_NOTIFICATION_ICONID = "notification_iconid";
String NOTIFICATION_PICTURE_PATH = "notification_picture_path";
String EXTRA_NOTIFICATION_DNDSUPPRESSED = "notification_dndsuppressed";
String EXTRA_NOTIFICATION_CHANNEL_ID = "notification_channel_id";
String EXTRA_NOTIFICATION_CATEGORY = "notification_category";
String EXTRA_FIND_START = "find_start";
String EXTRA_VIBRATION_INTENSITY = "vibration_intensity";
String EXTRA_CALL_COMMAND = "call_command";

View File

@ -83,6 +83,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.Ga
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner255SMusicCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner265Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner265SCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner45Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner55Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner620Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner955Coordinator;
@ -228,6 +229,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaXmwsdj04Coo
import nodomain.freeyourgadget.gadgetbridge.devices.miscale.MiCompositionScaleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miscale.MiSmartScaleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.moondrop.MoondropSpaceTravelCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.ColmiI28UltraCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.MisirunC17Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.CmfBudsPro2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear1Coordinator;
@ -457,6 +460,7 @@ public enum DeviceType {
GARMIN_FENIX_7X(GarminFenix7XCoordinator.class),
GARMIN_FENIX_7_PRO(GarminFenix7ProCoordinator.class),
GARMIN_FENIX_8(GarminFenix8Coordinator.class),
GARMIN_FORERUNNER_45(GarminForerunner45Coordinator.class),
GARMIN_FORERUNNER_55(GarminForerunner55Coordinator.class),
GARMIN_FORERUNNER_165(GarminForerunner165Coordinator.class),
GARMIN_FORERUNNER_235(GarminForerunner235Coordinator.class),
@ -586,6 +590,8 @@ public enum DeviceType {
COLMI_R06(ColmiR06Coordinator.class),
COLMI_R09(ColmiR09Coordinator.class),
COLMI_R10(ColmiR10Coordinator.class),
COLMI_I28_ULTRA(ColmiI28UltraCoordinator.class),
MISIRUN_C17(MisirunC17Coordinator.class),
B_AND_W_P_SERIES(BandWPSeriesDeviceCoordinator.class),
SCANNABLE(ScannableDeviceCoordinator.class),
CYCLING_SENSOR(CyclingSensorCoordinator.class),

View File

@ -34,6 +34,8 @@ public class NotificationSpec {
public String body;
public NotificationType type;
public String sourceName;
public String channelId;
public String category;
public String[] cannedReplies;
/**
* Wearable actions that were attached to the incoming notifications and will be passed to the gadget (includes the "reply" action)

View File

@ -864,6 +864,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.iconId = intentCopy.getIntExtra(EXTRA_NOTIFICATION_ICONID, 0);
notificationSpec.picturePath = intent.getStringExtra(NOTIFICATION_PICTURE_PATH);
notificationSpec.dndSuppressed = intentCopy.getIntExtra(EXTRA_NOTIFICATION_DNDSUPPRESSED, 0);
notificationSpec.channelId = intentCopy.getStringExtra(EXTRA_NOTIFICATION_CHANNEL_ID);
notificationSpec.category = intentCopy.getStringExtra(EXTRA_NOTIFICATION_CATEGORY);
if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) {
GBApplication.getIDSenderLookup().add(notificationSpec.getId(), notificationSpec.phoneNumber);

View File

@ -212,6 +212,14 @@ public class BLETypeConversions {
return (short) (bytes[0] & 0xff | ((bytes[1] & 0xff) << 8));
}
public static int toUint24(byte... bytes) {
return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8) | ((bytes[2] & 0xff) << 16);
}
public static int toUint24(byte[] bytes, int offset) {
return (bytes[offset + 0] & 0xff) | ((bytes[offset + 1] & 0xff) << 8) | ((bytes[offset + 2] & 0xff) << 16);
}
public static int toUint32(byte... bytes) {
return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8) | ((bytes[2] & 0xff) << 16) | ((bytes[3] & 0xff) << 24);
}

View File

@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.asteroidos;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import androidx.annotation.NonNull;
@ -274,8 +275,9 @@ public class AsteroidOSDeviceSupport extends AbstractBTLEDeviceSupport {
*/
public void handleMediaCommand (BluetoothGattCharacteristic characteristic) {
LOG.info("handle media command");
AsteroidOSMediaCommand command = new AsteroidOSMediaCommand(characteristic.getValue()[0]);
AsteroidOSMediaCommand command = new AsteroidOSMediaCommand(characteristic.getValue(), getContext());
GBDeviceEventMusicControl event = command.toMusicControlEvent();
if (event != null)
evaluateGBDeviceEvent(event);
}

View File

@ -63,14 +63,15 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetBatteryLevelRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetPhoneInfoRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadComplete;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsStatusRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMenstrualModifyTimeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadAck;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadChunk;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadHash;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendPermissionCheckResponse;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWatchfaceConfirm;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWatchfaceOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMusicStatusRequest;
@ -129,6 +130,7 @@ public class AsynchronousResponse {
handleEphemerisUploadService(response);
handleAsyncBattery(response);
handleNotifications(response);
handlePermissionCheck(response);
} catch (Request.ResponseParseException e) {
LOG.error("Response parse exception", e);
}
@ -737,4 +739,29 @@ public class AsynchronousResponse {
support.getHuaweiNotificationsManager().onReplyResponse((Notifications.NotificationReply.ReplyResponse) response);
}
}
private void handlePermissionCheck(HuaweiPacket response) {
if (response.serviceId == DeviceConfig.id && response.commandId == DeviceConfig.PermissionCheck.id) {
if (!(response instanceof DeviceConfig.PermissionCheck.PermissionCheckRequest)) {
return;
}
DeviceConfig.PermissionCheck.PermissionCheckRequest permissionCheckResp = (DeviceConfig.PermissionCheck.PermissionCheckRequest) response;
// short status = 1;
// // TODO: we should check ability to perform specific action. I do not know which action can be here,
// // 1 is SMS permission
// if(permissionCheckResp.permission == 1) {
// status = 0;
// }
// TODO: return no permission for now. Return status 1 for activate call reject replies. Something should be set on notification to enable processing.
// Currently watch does not send call reject to the GB. Additional research required.
short status = 0;
SendPermissionCheckResponse getPhoneInfoReq = new SendPermissionCheckResponse(this.support, permissionCheckResp.permission, status);
try {
getPhoneInfoReq.doPerform();
} catch (IOException e) {
LOG.error("Failed to send permission check ACK", e);
}
}
}
}

View File

@ -31,6 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
@ -215,4 +216,9 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
supportProvider.onMusicOperation(operation, playlistIndex, playlistName, musicIds);
}
@Override
public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
supportProvider.onSetCannedMessages(cannedMessagesSpec);
}
}

View File

@ -35,6 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
@ -224,4 +225,9 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport {
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
supportProvider.onMusicOperation(operation, playlistIndex, playlistName, musicIds);
}
@Override
public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
supportProvider.onSetCannedMessages(cannedMessagesSpec);
}
}

View File

@ -16,6 +16,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotificationRemoveRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendSMSReplyAck;
public class HuaweiNotificationsManager {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiNotificationsManager.class);
@ -42,6 +43,12 @@ public class HuaweiNotificationsManager {
notificationSpecCache.offer(notificationSpec);
}
public static String getNotificationKey(NotificationSpec notificationSpec) {
if(!TextUtils.isEmpty(notificationSpec.key)) {
return notificationSpec.key;
}
return "0|" + notificationSpec.sourceAppId + "|" + notificationSpec.getId() + "||0";
}
public void onNotification(NotificationSpec notificationSpec) {
@ -79,10 +86,10 @@ public class HuaweiNotificationsManager {
SendNotificationRemoveRequest sendNotificationReq = new SendNotificationRemoveRequest(this.support,
SendNotificationRequest.getNotificationType(notificationSpec.type), // notificationType
notificationSpec.sourceAppId,
notificationSpec.key,
getNotificationKey(notificationSpec),
id,
"", // TODO:
null);
notificationSpec.channelId,
notificationSpec.category);
sendNotificationReq.doPerform();
} catch (IOException e) {
LOG.error("Sending notification remove failed", e);
@ -91,7 +98,7 @@ public class HuaweiNotificationsManager {
void onReplyResponse(Notifications.NotificationReply.ReplyResponse response) {
LOG.info(" KEY: {}, Text: {}", response.key, response.text);
if(!this.support.getHuaweiCoordinator().supportsNotificationsReply()) {
if(!this.support.getHuaweiCoordinator().supportsNotificationsReplyActions()) {
LOG.info("Reply is not supported");
return;
}
@ -99,17 +106,26 @@ public class HuaweiNotificationsManager {
LOG.info("Reply is empty");
return;
}
if(response.type != 1 && response.type != 2) {
LOG.info("Reply: only type 1 and 2 supported");
return;
}
NotificationSpec notificationSpec = null;
if(response.type == 1) { // generic SMS notification reply. Find by phone number
for (NotificationSpec spec : notificationSpecCache) {
if (spec.phoneNumber.equals(response.key)) {
notificationSpec = spec;
if (notificationSpec.key.equals(response.key)) {
break;
}
}
} else if(response.type == 2) {
for (NotificationSpec spec : notificationSpecCache) {
if (getNotificationKey(spec).equals(response.key)) {
notificationSpec = spec;
break;
}
}
} else {
LOG.info("Reply type {} is not supported", response.type);
return;
}
if (notificationSpec == null) {
LOG.info("Notification for reply is not found");
return;
@ -131,10 +147,23 @@ public class HuaweiNotificationsManager {
}
}
}
}
this.support.evaluateGBDeviceEvent(deviceEvtNotificationControl);
//TODO: maybe should be send reply. Service: 0x2, command: 0x10, tlv 7 and/or 1, type byte, 7f on error
if(response.type == 1) {
// NOTE: send response only for SMS reply
try {
// 0xff - OK
// 0x7f - error
// TODO: get response from SMSManager. Send pending intent result.
// result can be one of the RESULT_ERROR_* from SmsManager. Not sure, need to check.
// currently always send OK.
byte resultCode = (byte)0xff;
SendSMSReplyAck sendNotificationReq = new SendSMSReplyAck(this.support, resultCode);
sendNotificationReq.doPerform();
} catch (IOException e) {
LOG.error("Sending sns reply ACK", e);
}
}
}
}

View File

@ -98,6 +98,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
@ -108,6 +109,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PCalendarService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PCannedRepliesService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PTrackService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PDataDictionarySyncService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.AcceptAgreementsRequest;
@ -865,6 +867,12 @@ public class HuaweiSupportProvider {
trackService.register();
}
}
if (getHuaweiCoordinator().supportsCannedReplies()) {
if (HuaweiP2PCannedRepliesService.getRegisteredInstance(huaweiP2PManager) == null) {
HuaweiP2PCannedRepliesService cannedRepliesService = new HuaweiP2PCannedRepliesService(huaweiP2PManager);
cannedRepliesService.register();
}
}
if (HuaweiP2PDataDictionarySyncService.getRegisteredInstance(huaweiP2PManager) == null) {
HuaweiP2PDataDictionarySyncService trackService = new HuaweiP2PDataDictionarySyncService(huaweiP2PManager);
trackService.register();
@ -2514,4 +2522,23 @@ public class HuaweiSupportProvider {
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
getHuaweiMusicManager().onMusicOperation(operation, playlistIndex, playlistName, musicIds);
}
public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
if (cannedMessagesSpec.type != CannedMessagesSpec.TYPE_GENERIC) {
LOG.warn("Got unsupported canned messages type: {}", cannedMessagesSpec.type);
return;
}
if(cannedMessagesSpec.cannedMessages.length == 0) {
GB.toast(context, HuaweiSupportProvider.this.getContext().getString(R.string.canned_replies_not_empty), Toast.LENGTH_SHORT, GB.WARN);
LOG.warn(HuaweiSupportProvider.this.getContext().getString(R.string.canned_replies_not_empty));
}
HuaweiP2PCannedRepliesService cannedRepliesService = HuaweiP2PCannedRepliesService.getRegisteredInstance(huaweiP2PManager);
if(cannedRepliesService == null) {
LOG.warn("P2P canned replies service is not registered");
return;
}
cannedRepliesService.sendReplies(cannedMessagesSpec.cannedMessages);
}
}

View File

@ -91,14 +91,14 @@ public abstract class HuaweiBaseP2PService {
}
public void handlePacket(P2P.P2PCommand.Response packet) {
LOG.info("HuaweiP2PCalendarService handlePacket: {} Code: {}", packet.cmdId, packet.respCode);
LOG.info("HuaweiBaseP2PService handlePacket: {} Code: {}", packet.cmdId, packet.respCode);
if (waitPackets.containsKey(packet.sequenceId)) {
LOG.info("HuaweiP2PCalendarService handlePacket find handler");
LOG.info("HuaweiBaseP2PService handlePacket find handler");
HuaweiP2PCallback handle = waitPackets.remove(packet.sequenceId);
if(handle != null) {
handle.onResponse(packet.respCode, packet.respData);
} else {
LOG.error("HuaweiP2PCalendarService handler is null");
LOG.error("HuaweiBaseP2PService handler is null");
}
} else {

View File

@ -0,0 +1,138 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p;
import android.text.TextUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiP2PManager;
public class HuaweiP2PCannedRepliesService extends HuaweiBaseP2PService {
private final Logger LOG = LoggerFactory.getLogger(HuaweiP2PCannedRepliesService.class);
private final AtomicBoolean isRegistered = new AtomicBoolean(false);
public static final String MODULE = "hw.unitedevice.smsquick";
public static final int CMD_CANNED_REPLY_QUERY = 7001;
public static final int CMD_CANNED_REPLY_UPDATE = 7002;
public static final int CMD_CANNED_REPLY_CONNECT = 7003;
public HuaweiP2PCannedRepliesService(HuaweiP2PManager manager) {
super(manager);
LOG.info("HuaweiP2PCannedRepliesService");
}
public static HuaweiP2PCannedRepliesService getRegisteredInstance(HuaweiP2PManager manager) {
return (HuaweiP2PCannedRepliesService) manager.getRegisteredService(HuaweiP2PCannedRepliesService.MODULE);
}
@Override
public String getModule() {
return HuaweiP2PCannedRepliesService.MODULE;
}
@Override
public String getPackage() {
return "com.huawei.watch.home";
}
@Override
public String getFingerprint() {
return "603AC6A57E2023E00C9C93BB539CA653DF3003EBA4E92EA1904BA4AAA5D938F0";
}
@Override
public void registered() {
isRegistered.set(true);
// NOTE: sendConnect can clean saved canned messages. Additional research required
//sendConnect();
sendQuery();
}
@Override
public void unregister() {
isRegistered.set(false);
}
public void sendConnect() {
HuaweiTLV tlv = new HuaweiTLV();
tlv.put(0x1, CMD_CANNED_REPLY_CONNECT);
sendCommand(tlv.serialize(), null);
}
public void sendQuery() {
HuaweiTLV tlv = new HuaweiTLV();
tlv.put(0x1, CMD_CANNED_REPLY_QUERY);
sendCommand(tlv.serialize(), null);
}
public void sendReplies(String[] replies) {
HuaweiTLV tlv = new HuaweiTLV();
for (String reply : replies) {
tlv.put(0x83, new HuaweiTLV().put(0x04, reply));
}
HuaweiTLV res = new HuaweiTLV();
res.put(0x1, CMD_CANNED_REPLY_UPDATE).put(0x82, tlv);
sendCommand(res.serialize(), null);
}
private void parseDeviceReplies(HuaweiTLV tlv) {
List<HuaweiTLV> replies = tlv.getObjects(0x83);
if (replies.isEmpty())
return;
final GBDeviceEventUpdatePreferences gbDeviceEventUpdatePreferences = new GBDeviceEventUpdatePreferences();
for (int i = 1; i <= manager.getSupportProvider().getHuaweiCoordinator().getCannedRepliesSlotCount(manager.getSupportProvider().getDevice()); i++) {
String message = null;
if (replies.size() >= i) {
if (replies.get(i - 1).contains(0x04)) {
try {
String reply = replies.get(i - 1).getString(0x04);
if (!TextUtils.isEmpty(reply)) {
message = reply;
}
} catch (HuaweiPacket.MissingTagException e) {
LOG.info("No tag");
}
}
}
gbDeviceEventUpdatePreferences.withPreference("canned_reply_" + i, message);
}
manager.getSupportProvider().evaluateGBDeviceEvent(gbDeviceEventUpdatePreferences);
}
@Override
public void handleData(byte[] data) {
LOG.info("HuaweiP2PCannedRepliesService handleData");
try {
HuaweiTLV tlv = new HuaweiTLV();
tlv.parse(data);
LOG.error(tlv.toString());
if (tlv.contains(0x01)) {
int code = tlv.getInteger(0x01);
if (code == CMD_CANNED_REPLY_CONNECT) {
// send default replies, replies cannot be empty
String[] replies = {"OK", "Yes", "No"};
sendReplies(replies);
}
if (code == CMD_CANNED_REPLY_QUERY) {
if (tlv.contains(0x82)) {
parseDeviceReplies(tlv.getObject(0x82));
}
}
}
} catch (HuaweiPacket.MissingTagException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -16,6 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiNotificationsManager.getNotificationKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -29,7 +31,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendNotificationRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(SendNotificationRequest.class);
private HuaweiPacket packet;
@ -53,7 +54,6 @@ public class SendNotificationRequest extends Request {
}
}
public void buildNotificationTLVFromNotificationSpec(NotificationSpec notificationSpec) {
String title;
if (notificationSpec.title != null)
@ -66,16 +66,34 @@ public class SendNotificationRequest extends Request {
body = notificationSpec.body.substring(0x0, supportProvider.getHuaweiCoordinator().getContentLength() - 0xD);
body += "...";
}
String replyKey = "";
final boolean hasActions = (null != notificationSpec.attachedActions && !notificationSpec.attachedActions.isEmpty());
if (hasActions) {
for (int i = 0; i < notificationSpec.attachedActions.size(); i++) {
final NotificationSpec.Action action = notificationSpec.attachedActions.get(i);
if (action.type == NotificationSpec.Action.TYPE_WEARABLE_REPLY || action.type == NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
//NOTE: store notification key instead action key. The watch returns this key so it is more easier to find action by notification key
replyKey = getNotificationKey(notificationSpec);
break;
}
}
}
Notifications.NotificationActionRequest.AdditionalParams params = new Notifications.NotificationActionRequest.AdditionalParams();
params.supportsSyncKey = supportProvider.getHuaweiCoordinator().supportsNotificationsSyncKey();
params.supportsReply = supportProvider.getHuaweiCoordinator().supportsNotificationsReply();
params.supportsRepeatedNotify = supportProvider.getHuaweiCoordinator().supportsNotificationsRepeatedNotify();
params.supportsRemoveSingle = supportProvider.getHuaweiCoordinator().supportsNotificationsRemoveSingle();
params.supportsReply = supportProvider.getHuaweiCoordinator().supportsNotificationsReply();
params.supportsReplyActions = supportProvider.getHuaweiCoordinator().supportsNotificationsReplyActions();
params.supportsTimestamp = supportProvider.getHuaweiCoordinator().supportsNotificationsTimestamp();
params.notificationId = notificationSpec.getId();
params.notificationKey = notificationSpec.key;
params.notificationKey = getNotificationKey(notificationSpec);
params.replyKey = replyKey;
params.channelId = notificationSpec.channelId;
params.category = notificationSpec.category;
params.address = notificationSpec.phoneNumber;
this.packet = new Notifications.NotificationActionRequest(
@ -92,7 +110,6 @@ public class SendNotificationRequest extends Request {
}
public void buildNotificationTLVFromCallSpec(CallSpec callSpec) {
this.packet = new Notifications.NotificationActionRequest(
paramsProvider,
supportProvider.getNotificationId(),

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendPermissionCheckResponse extends Request {
short permission;
short status;
public SendPermissionCheckResponse(HuaweiSupportProvider support, short permission, short status) {
super(support);
this.serviceId = DeviceConfig.id;
this.commandId = DeviceConfig.PermissionCheck.id;
this.permission = permission;
this.status = status;
this.addToResponse = false;
}
@Override
protected List<byte[]> createRequest() throws Request.RequestCreationException {
try {
return new DeviceConfig.PermissionCheck.PermissionCheckResponse(this.paramsProvider, this.permission, this.status).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new Request.RequestCreationException(e);
}
}
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendSMSReplyAck extends Request {
private final byte resultCode;
public SendSMSReplyAck(HuaweiSupportProvider support,
byte resultCode) {
super(support);
this.serviceId = Notifications.id;
this.commandId = Notifications.NotificationReply.id;
this.resultCode = resultCode;
this.addToResponse = false;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Notifications.NotificationReply.ReplyAck(this.paramsProvider, this.resultCode).serialize();
} catch(HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e.getMessage());
}
}
}

View File

@ -0,0 +1,212 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.moyoung;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.MoyoungConstants;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FetchDataOperation extends AbstractBTLEOperation<MoyoungDeviceSupport> {
private static final Logger LOG = LoggerFactory.getLogger(FetchDataOperation.class);
private boolean[] receivedSteps = new boolean[3];
private boolean[] receivedSleep = new boolean[3];
private boolean receivedTrainingData = false;
private MoyoungPacketIn packetIn = new MoyoungPacketIn();
public FetchDataOperation(MoyoungDeviceSupport support) {
super(support);
}
@Override
protected void prePerform() {
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data));
getDevice().sendDeviceUpdateIntent(getContext());
}
@Override
protected void doPerform() throws IOException {
TransactionBuilder builder = performInitialized("FetchDataOperation");
getSupport().sendPacket(builder, MoyoungPacketOut.buildPacket(getSupport().getMtu(), MoyoungConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { MoyoungConstants.ARG_SYNC_YESTERDAY_SLEEP }));
getSupport().sendPacket(builder, MoyoungPacketOut.buildPacket(getSupport().getMtu(), MoyoungConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { MoyoungConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_SLEEP }));
getSupport().sendPacket(builder, MoyoungPacketOut.buildPacket(getSupport().getMtu(), MoyoungConstants.CMD_SYNC_SLEEP, new byte[0]));
getSupport().sendPacket(builder, MoyoungPacketOut.buildPacket(getSupport().getMtu(), MoyoungConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { MoyoungConstants.ARG_SYNC_YESTERDAY_STEPS }));
getSupport().sendPacket(builder, MoyoungPacketOut.buildPacket(getSupport().getMtu(), MoyoungConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { MoyoungConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_STEPS }));
builder.read(getCharacteristic(MoyoungConstants.UUID_CHARACTERISTIC_STEPS));
getSupport().sendPacket(builder, MoyoungPacketOut.buildPacket(getSupport().getMtu(), MoyoungConstants.CMD_QUERY_MOVEMENT_HEART_RATE, new byte[] { }));
getSupport().sendPacket(builder, MoyoungPacketOut.buildPacket(getSupport().getMtu(), MoyoungConstants.CMD_QUERY_PAST_HEART_RATE_1, new byte[] { 0x00 }));
builder.queue(getQueue());
updateProgressAndCheckFinish();
}
@Override
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (!isOperationRunning())
{
LOG.error("onCharacteristicRead but operation is not running!");
}
else
{
UUID charUuid = characteristic.getUuid();
if (charUuid.equals(MoyoungConstants.UUID_CHARACTERISTIC_STEPS)) {
byte[] data = characteristic.getValue();
LOG.info("TODAY STEPS data: " + Logging.formatBytes(data));
decodeSteps(0, data);
return true;
}
}
return super.onCharacteristicRead(gatt, characteristic, status);
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
if (!isOperationRunning())
{
LOG.error("onCharacteristicChanged but operation is not running!");
}
else
{
UUID charUuid = characteristic.getUuid();
if (charUuid.equals(MoyoungConstants.UUID_CHARACTERISTIC_DATA_IN))
{
if (packetIn.putFragment(characteristic.getValue())) {
Pair<Byte, byte[]> packet = MoyoungPacketIn.parsePacket(packetIn.getPacket());
packetIn = new MoyoungPacketIn();
if (packet != null) {
byte packetType = packet.first;
byte[] payload = packet.second;
if (handlePacket(packetType, payload))
return true;
}
}
}
}
return super.onCharacteristicChanged(gatt, characteristic);
}
private boolean handlePacket(byte packetType, byte[] payload) {
if (packetType == MoyoungConstants.CMD_SYNC_SLEEP) {
LOG.info("TODAY SLEEP data: " + Logging.formatBytes(payload));
decodeSleep(0, payload);
return true;
}
if (packetType == MoyoungConstants.CMD_SYNC_PAST_SLEEP_AND_STEP) {
byte dataType = payload[0];
byte[] data = new byte[payload.length - 1];
System.arraycopy(payload, 1, data, 0, data.length);
// NOTE: Does this seem swapped to you? That's because IT IS! I took the constant names
// from the official app, but as it turns out, the official app has a bug.
// (and yes, you can see that data from yesterday appears as two days ago
// in the app itself and all past data is getting messed up because of it)
if (dataType == MoyoungConstants.ARG_SYNC_YESTERDAY_STEPS) {
LOG.info("2 DAYS AGO STEPS data: " + Logging.formatBytes(data));
decodeSteps(2, data);
return true;
}
else if (dataType == MoyoungConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_STEPS) {
LOG.info("YESTERDAY STEPS data: " + Logging.formatBytes(data));
decodeSteps(1, data);
return true;
}
else if (dataType == MoyoungConstants.ARG_SYNC_YESTERDAY_SLEEP) {
LOG.info("2 DAYS AGO SLEEP data: " + Logging.formatBytes(data));
decodeSleep(2, data);
return true;
}
else if (dataType == MoyoungConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_SLEEP) {
LOG.info("YESTERDAY SLEEP data: " + Logging.formatBytes(data));
decodeSleep(1, data);
return true;
}
}
if (packetType == MoyoungConstants.CMD_QUERY_MOVEMENT_HEART_RATE) {
decodeTrainingData(payload);
return true;
}
return false;
}
private void decodeSteps(int daysAgo, byte[] data)
{
getSupport().handleStepsHistory(daysAgo, data, false);
receivedSteps[daysAgo] = true;
updateProgressAndCheckFinish();
}
private void decodeSleep(int daysAgo, byte[] data)
{
getSupport().handleSleepHistory(daysAgo, data);
receivedSleep[daysAgo] = true;
updateProgressAndCheckFinish();
}
private void decodeTrainingData(byte[] data)
{
getSupport().handleTrainingData(data);
receivedTrainingData = true;
updateProgressAndCheckFinish();
}
private void updateProgressAndCheckFinish()
{
int count = 0;
int total = receivedSteps.length + receivedSleep.length;
for(int i = 0; i < receivedSteps.length; i++)
if (receivedSteps[i])
++count;
for(int i = 0; i < receivedSleep.length; i++)
if (receivedSleep[i])
++count;
if (receivedTrainingData)
++count;
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 100 * count / total, getContext());
LOG.debug("Fetching activity data status: {} out of {}", count, total);
if (count == total)
operationFinished();
}
@Override
protected void operationFinished() {
operationStatus = OperationStatus.FINISHED;
if (getDevice() != null && getDevice().isConnected()) {
unsetBusy();
GB.signalActivityDataFinish(getDevice());
}
}
}

View File

@ -0,0 +1,22 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.moyoung;
public class MoyoungPacket {
protected byte[] packet;
protected int position = 0;
}

View File

@ -0,0 +1,135 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.moyoung;
import android.util.Pair;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.Logging;
/**
* A class for handling fragmentation of incoming packets<br>
* <br>
* Usage:
* <pre>
* {@code
* if(packetIn.putFragment(fragment)) {
* Pair<Byte, byte[]> packet = MoyoungPacketIn.parsePacket(packetIn.getPacket());
* packetIn = new MoyoungPacketIn();
* if (packet != null) {
* byte packetType = packet.first;
* byte[] payload = packet.second;
* // ...
* }
* }
* </pre>
*/
public class MoyoungPacketIn extends MoyoungPacket {
private static final Logger LOG = LoggerFactory.getLogger(MoyoungPacketIn.class);
public MoyoungPacketIn()
{
}
/**
* Store the incoming fragment and try to reconstruct packet
*
* @param fragment The incoming fragment
* @return true if the packet is complete
*/
public boolean putFragment(byte[] fragment)
{
if (packet == null)
{
int len = parsePacketLength(fragment);
if (len < 0)
return false; // corrupted packet
packet = new byte[len];
}
int toCopy = Math.min(fragment.length, packet.length - position);
if (fragment.length > toCopy)
{
LOG.warn("Got fragment with more data than expected!");
}
System.arraycopy(fragment, 0, packet, position, toCopy);
position += fragment.length;
return position >= packet.length;
}
public byte[] getPacket()
{
if (packet == null || position < packet.length)
throw new IllegalStateException("Packet is not complete yet");
return packet;
}
/**
* Parse the packet header and return the length
* @param packetOrFragment The entire packet or it's first fragment
* @return The packet length, or -1 if packet is corrupted
*/
private static int parsePacketLength(@NonNull byte[] packetOrFragment)
{
if (packetOrFragment[0] != (byte)0xFE || packetOrFragment[1] != (byte)0xEA)
{
LOG.warn("Invalid packet header, ignoring! Fragment: " + Logging.formatBytes(packetOrFragment));
return -1;
}
int len_h = 0;
if (packetOrFragment[2] != 16)
{
if ((packetOrFragment[2] & 0xFF) < 32)
{
LOG.warn("Corrupted packet, unable to parse length");
return -1;
}
len_h = (packetOrFragment[2] & 0xFF) - 32;
}
int len_l = (packetOrFragment[3] & 0xFF);
return (len_h << 8) | len_l;
}
/**
* Parse the packet
* @param packet The complete packet
* @return A pair containing the packet type and payload
*/
public static Pair<Byte, byte[]> parsePacket(@NonNull byte[] packet)
{
int len = parsePacketLength(packet);
if (len < 0)
return null;
if (len != packet.length)
{
LOG.warn("Invalid packet length!");
return null;
}
byte packetType = packet[4];
byte[] payload = new byte[packet.length - 5];
System.arraycopy(packet, 5, payload, 0, payload.length);
return Pair.create(packetType, payload);
}
}

View File

@ -0,0 +1,81 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.moyoung;
import androidx.annotation.NonNull;
/**
* A class for handling fragmentation of outgoing packets<br>
* <br>
* Usage:
* <pre>
* {@code
* MoyoungPacketOut packetOut = new MoyoungPacketOut(MoyoungPacketOut.buildPacket(type, payload));
* byte[] fragment = new byte[MTU];
* while(packetOut.getFragment(fragment))
* send(fragment);
* }
* </pre>
*/
public class MoyoungPacketOut extends MoyoungPacket {
public MoyoungPacketOut(byte[] packet)
{
this.packet = packet;
}
/**
* Get the next fragment of this packet to be sent
*
* @param fragmentBuffer The buffer to store the output in, of desired size (i.e. == MTU)
* @return true if there is more data to be sent, false otherwise
*/
public boolean getFragment(byte[] fragmentBuffer)
{
if (position >= packet.length)
return false;
int remainingToTransfer = Math.min(fragmentBuffer.length, packet.length - position);
System.arraycopy(packet, position, fragmentBuffer, 0, remainingToTransfer);
position += remainingToTransfer;
return true;
}
/**
* Encode the packet
* @param packetType The packet type
* @param payload The packet payload
* @return The encoded packet
*/
public static byte[] buildPacket(int mtu, byte packetType, @NonNull byte[] payload)
{
byte[] packet = new byte[payload.length + 5];
packet[0] = (byte)0xFE;
packet[1] = (byte)0xEA;
if (mtu == 20)
{
packet[2] = 16;
packet[3] = (byte)(packet.length & 0xFF);
}
else
{
packet[2] = (byte)(32 + (packet.length >> 8) & 0xFF);
packet[3] = (byte)(packet.length & 0xFF);
}
packet[4] = packetType;
System.arraycopy(payload, 0, packet, 5, payload.length);
return packet;
}
}

View File

@ -0,0 +1,139 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.moyoung;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.AbstractMoyoungDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.MoyoungConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungSetting;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus;
public class QuerySettingsOperation extends AbstractBTLEOperation<MoyoungDeviceSupport> {
private static final Logger LOG = LoggerFactory.getLogger(QuerySettingsOperation.class);
private final MoyoungSetting[] settingsToQuery;
private boolean[] received;
private MoyoungPacketIn packetIn = new MoyoungPacketIn();
public QuerySettingsOperation(MoyoungDeviceSupport support, MoyoungSetting[] settingsToQuery) {
super(support);
this.settingsToQuery = settingsToQuery;
}
public QuerySettingsOperation(MoyoungDeviceSupport support) {
super(support);
AbstractMoyoungDeviceCoordinator coordinator = (AbstractMoyoungDeviceCoordinator) getDevice().getDeviceCoordinator();
this.settingsToQuery = coordinator.getSupportedSettings();
}
@Override
protected void prePerform() {
getDevice().setBusyTask("Querying settings"); // mark as busy quickly to avoid interruptions from the outside
getDevice().sendDeviceUpdateIntent(getContext());
}
@Override
protected void doPerform() throws IOException {
received = new boolean[settingsToQuery.length];
TransactionBuilder builder = performInitialized("QuerySettingsOperation");
for (MoyoungSetting setting : settingsToQuery)
{
if (setting.cmdQuery == -1)
continue;
getSupport().sendPacket(builder, MoyoungPacketOut.buildPacket(getSupport().getMtu(), setting.cmdQuery, new byte[0]));
}
builder.queue(getQueue());
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
if (!isOperationRunning())
{
LOG.error("onCharacteristicChanged but operation is not running!");
}
else
{
UUID charUuid = characteristic.getUuid();
if (charUuid.equals(MoyoungConstants.UUID_CHARACTERISTIC_DATA_IN))
{
if (packetIn.putFragment(characteristic.getValue())) {
Pair<Byte, byte[]> packet = MoyoungPacketIn.parsePacket(packetIn.getPacket());
packetIn = new MoyoungPacketIn();
if (packet != null) {
byte packetType = packet.first;
byte[] payload = packet.second;
if (handlePacket(packetType, payload))
return true;
}
}
}
}
return super.onCharacteristicChanged(gatt, characteristic);
}
private boolean handlePacket(byte packetType, byte[] payload) {
boolean handled = false;
boolean receivedEverything = true;
for(int i = 0; i < settingsToQuery.length; i++)
{
MoyoungSetting setting = settingsToQuery[i];
if (setting.cmdQuery == -1)
continue;
if (setting.cmdQuery == packetType)
{
try {
Object value = setting.decode(payload);
LOG.info("SETTING QUERY " + setting.name + " = " + value.toString());
} catch (Exception e) {
LOG.error("Parse error in packet for setting " + setting.name + ": ", e);
}
received[i] = true;
handled = true;
}
else if (!received[i])
receivedEverything = false;
}
if (receivedEverything)
operationFinished();
return handled;
}
@Override
protected void operationFinished() {
operationStatus = OperationStatus.FINISHED;
if (getDevice() != null && getDevice().isConnected()) {
unsetBusy();
}
}
}

View File

@ -0,0 +1,221 @@
/* Copyright (C) 2019 krzys_h
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.moyoung;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Pair;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Calendar;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.MoyoungConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungHeartRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungHeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class TrainingFinishedDataOperation extends AbstractBTLEOperation<MoyoungDeviceSupport> {
private static final Logger LOG = LoggerFactory.getLogger(TrainingFinishedDataOperation.class);
private final byte[] firstPacketData;
private final long firstPacketTimeInMillis;
ByteArrayOutputStream data = new ByteArrayOutputStream();
private MoyoungPacketIn packetIn = new MoyoungPacketIn();
public TrainingFinishedDataOperation(MoyoungDeviceSupport support, byte[] firstPacketData) {
super(support);
this.firstPacketData = firstPacketData;
this.firstPacketTimeInMillis = Calendar.getInstance().getTimeInMillis();
}
@Override
protected void prePerform() {
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_training_data));
getDevice().sendDeviceUpdateIntent(getContext());
}
@Override
protected void doPerform() {
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_training_data), true, 0, getContext());
handleTrainingHealthRatePacket(firstPacketData, true);
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
if (!isOperationRunning())
{
LOG.error("onCharacteristicChanged but operation is not running!");
}
else
{
UUID charUuid = characteristic.getUuid();
if (charUuid.equals(MoyoungConstants.UUID_CHARACTERISTIC_DATA_IN))
{
if (packetIn.putFragment(characteristic.getValue())) {
Pair<Byte, byte[]> packet = MoyoungPacketIn.parsePacket(packetIn.getPacket());
packetIn = new MoyoungPacketIn();
if (packet != null) {
byte packetType = packet.first;
byte[] payload = packet.second;
if (handlePacket(packetType, payload))
return true;
}
}
}
}
return super.onCharacteristicChanged(gatt, characteristic);
}
private boolean handlePacket(byte packetType, byte[] payload) {
if (packetType == MoyoungConstants.CMD_QUERY_LAST_DYNAMIC_RATE) {
handleTrainingHealthRatePacket(payload, false);
return true;
}
if (packetType == MoyoungConstants.CMD_QUERY_MOVEMENT_HEART_RATE) {
handleTrainingSummaryDataPacket(payload);
return true;
}
return false;
}
private void handleTrainingHealthRatePacket(byte[] payload, boolean isFirst) {
LOG.info("TRAINING DATA: " + Logging.formatBytes(payload));
byte sequenceType = payload[0];
if (isFirst != (sequenceType == MoyoungConstants.ARG_TRANSMISSION_FIRST))
throw new IllegalArgumentException("Expected packet to be " + (isFirst ? "first" : "continued") + " but got packet of type " + sequenceType);
if (sequenceType == MoyoungConstants.ARG_TRANSMISSION_LAST && payload.length > 1)
throw new IllegalArgumentException("Last packet shouldn't have any data");
data.write(payload, 1, payload.length - 1);
if (sequenceType != MoyoungConstants.ARG_TRANSMISSION_LAST)
queryMoreData();
else
processAllData();
}
private void queryMoreData() {
try {
TransactionBuilder builder = performInitialized("TrainingFinishedDataOperation");
getSupport().sendPacket(builder, MoyoungPacketOut.buildPacket(getSupport().getMtu(), MoyoungConstants.CMD_QUERY_LAST_DYNAMIC_RATE, new byte[0]));
builder.queue(getQueue());
} catch (IOException e) {
LOG.error("Error fetching training data: ", e);
GB.toast(getContext(), "Error fetching training data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
operationFinished();
}
}
private void processAllData() {
byte[] completeData = data.toByteArray();
LOG.info("HAVE COMPLETE DATA: " + Logging.formatBytes(completeData));
ByteBuffer dataBuffer = ByteBuffer.wrap(completeData);
dataBuffer.order(ByteOrder.LITTLE_ENDIAN);
Calendar dateRecorded = Calendar.getInstance();
dateRecorded.setTime(MoyoungConstants.WatchTimeToLocalTime(dataBuffer.getInt()));
// NOTE: The first sample always matches dateRecorded (which is aligned to the minute)
// The last sample is saved at the moment the recording is stopped (and this code starts executing)
try (DBHandler dbHandler = GBApplication.acquireDB()) {
User user = DBHelper.getUser(dbHandler.getDaoSession());
Device device = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession());
MoyoungHeartRateSampleProvider provider = new MoyoungHeartRateSampleProvider(getDevice(), dbHandler.getDaoSession());
LOG.info("START DATE: " + dateRecorded.getTime().toString());
while (dataBuffer.hasRemaining())
{
int measurement = dataBuffer.get() & 0xFF;
if (!dataBuffer.hasRemaining())
dateRecorded.setTimeInMillis(firstPacketTimeInMillis); // the last sample is captured exactly at the end of measurement
LOG.info("MEASUREMENT: at " + dateRecorded.getTime().toString() + " was " + measurement);
MoyoungHeartRateSample sample = new MoyoungHeartRateSample();
sample.setDevice(device);
sample.setUser(user);
sample.setTimestamp((int)(dateRecorded.getTimeInMillis() / 1000));
sample.setHeartRate(measurement != 0 ? measurement : ActivitySample.NOT_MEASURED);
provider.addSample(sample);
LOG.info("Adding a training sample: " + sample.toString());
dateRecorded.add(Calendar.MINUTE, 1);
}
} catch (Exception ex) {
LOG.error("Error saving samples: ", ex);
GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
}
try {
TransactionBuilder builder = performInitialized("TrainingFinishedDataOperation fetch training type");
getSupport().sendPacket(builder, MoyoungPacketOut.buildPacket(getSupport().getMtu(), MoyoungConstants.CMD_QUERY_MOVEMENT_HEART_RATE, new byte[] { }));
builder.queue(getQueue());
} catch (IOException e) {
LOG.error("Error fetching training data: ", e);
GB.toast(getContext(), "Error fetching training data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
operationFinished();
}
}
private void handleTrainingSummaryDataPacket(byte[] payload)
{
getSupport().handleTrainingData(payload);
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_training_data_finished), false, 0, getContext());
operationFinished();
}
@Override
protected void operationFinished() {
operationStatus = OperationStatus.FINISHED;
if (getDevice() != null && getDevice().isConnected()) {
unsetBusy();
GB.signalActivityDataFinish(getDevice());
}
}
}

View File

@ -120,4 +120,31 @@ public class ArrayUtils {
}
return -1;
}
/**
* Check if a byte array contains all zeros
* @param array The array to check
* @param startIndex The starting position
* @param length Number of elements to check
* @return true if all checked elements were == 0, false otherwise
*/
public static boolean isAllZeros(byte[] array, int startIndex, int length)
{
for(int i = startIndex; i < startIndex + length; i++)
{
if (array[i] != 0)
return false;
}
return true;
}
/**
* Check if a byte array contains all zeros
* @param array The array to check
* @return true if all checked elements were == 0, false otherwise
*/
public static boolean isAllZeros(byte[] array)
{
return isAllZeros(array, 0, array.length);
}
}

View File

@ -17,6 +17,7 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
@ -28,6 +29,7 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.widget.Toast;
@ -52,11 +54,15 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
public class PermissionsUtils {
private static final Logger LOG = LoggerFactory.getLogger(PermissionsUtils.class);
public static final String CUSTOM_PERM_IGNORE_BATT_OPTIM = "custom_perm_ignore_battery_optimization";
public static final String CUSTOM_PERM_NOTIFICATION_LISTENER = "custom_perm_notifications_listener";
public static final String CUSTOM_PERM_NOTIFICATION_SERVICE = "custom_perm_notifications_service";
public static final String CUSTOM_PERM_DISPLAY_OVER = "custom_perm_display_over";
public static final List<String> specialPermissions = new ArrayList<String>() {{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
add(CUSTOM_PERM_IGNORE_BATT_OPTIM);
}
add(CUSTOM_PERM_NOTIFICATION_LISTENER);
add(CUSTOM_PERM_NOTIFICATION_SERVICE);
add(CUSTOM_PERM_DISPLAY_OVER);
@ -68,6 +74,12 @@ public class PermissionsUtils {
public static ArrayList<PermissionDetails> getRequiredPermissionsList(Activity activity) {
ArrayList<PermissionDetails> permissionsList = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
permissionsList.add(new PermissionDetails(
CUSTOM_PERM_IGNORE_BATT_OPTIM,
activity.getString(R.string.permission_disable_doze_title),
activity.getString(R.string.permission_disable_doze_summary)));
}
permissionsList.add(new PermissionDetails(
CUSTOM_PERM_NOTIFICATION_LISTENER,
activity.getString(R.string.menuitem_notifications),
@ -189,6 +201,9 @@ public class PermissionsUtils {
return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted();
} else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Settings.canDrawOverlays(context);
} else if (permission.equals(CUSTOM_PERM_IGNORE_BATT_OPTIM) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return pm.isIgnoringBatteryOptimizations(context.getApplicationContext().getPackageName());
} else {
return ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_DENIED;
}
@ -205,7 +220,9 @@ public class PermissionsUtils {
}
public static void requestPermission(Activity activity, String permission) {
if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
if (permission.equals(CUSTOM_PERM_IGNORE_BATT_OPTIM)) {
showRequestIgnoreBatteryOptimizationDialog(activity);
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
showNotifyListenerPermissionsDialog(activity);
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)) {
showNotifyPolicyPermissionsDialog(activity);
@ -242,6 +259,14 @@ public class PermissionsUtils {
}
}
@SuppressLint("BatteryLife")
private static void showRequestIgnoreBatteryOptimizationDialog(Activity activity) {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + activity.getApplicationContext().getPackageName()));
activity.startActivity(intent);
}
private static void showNotifyListenerPermissionsDialog(Activity activity) {
new MaterialAlertDialogBuilder(activity)
.setMessage(activity.getString(R.string.permission_notification_listener,

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#7E7E7E"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M8,19h3v4h2v-4h3l-4,-4 -4,4zM16,5h-3L13,1h-2v4L8,5l4,4 4,-4zM4,11v2h16v-2L4,11z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#7E7E7E"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M4,18v3h3v-3h10v3h3v-6L4,15zM19,10h3v3h-3zM2,10h3v3L2,13zM17,13L7,13L7,5c0,-1.1 0.9,-2 2,-2h6c1.1,0 2,0.9 2,2v8z"/>
</vector>

View File

@ -1,58 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity$PlaceholderFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="40"
android:orientation="vertical">
<TextView
android:id="@+id/balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.github.mikephil.charting.charts.PieChart
android:id="@+id/todaystepschart"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="40" />
</LinearLayout>
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/weekstepschart"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="20" />
<!--<TextView-->
<!--android:text="Test"-->
<!--android:layout_width="fill_parent"-->
<!--android:layout_height="fill_parent"-->
<!--android:layout_weight="20" />-->
</LinearLayout>
<ImageView
android:id="@+id/steps_streaks_button"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:layout_marginStart="5dp"
android:layout_marginEnd="0dp"
android:visibility="visible"
app:srcCompat="@drawable/ic_events" />
</RelativeLayout>

View File

@ -20,10 +20,21 @@
android:gravity="center"
android:textSize="20sp"
android:layout_marginTop="15dp"
android:layout_marginBottom="20dp"
android:layout_marginBottom="10dp"
/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center">
<TextView
android:id="@+id/balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="250sp"

View File

@ -1,44 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity$PlaceholderFragment">
<TextView
android:id="@+id/balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.github.mikephil.charting.charts.PieChart
android:id="@+id/todaystepschart"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="20" />
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/weekstepschart"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="20" />
</LinearLayout>
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:id="@+id/steps_streaks_button"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginTop="0dp"
android:layout_marginEnd="0dp"
android:visibility="visible"
app:srcCompat="@drawable/ic_events" />
</RelativeLayout>

View File

@ -366,6 +366,16 @@
<item>@string/p_scheduled</item>
</string-array>
<string-array name="sedentary_reminder">
<item>@string/off</item>
<item>@string/on</item>
</string-array>
<string-array name="sedentary_reminder_values">
<item>@string/p_off</item>
<item>@string/p_on</item>
</string-array>
<string-array name="mi2_do_not_disturb">
<item>@string/mi2_dnd_off</item>
<item>@string/mi2_dnd_automatic</item>
@ -2624,6 +2634,44 @@
<item>ru_RU</item>
</string-array>
<string-array name="pref_moyoung_watch_face">
<item>Watch face 1</item>
<item>Watch face 2</item>
<item>Watch face 3</item>
<item>Watch face 4</item>
<item>Watch face 5</item>
<item>Watch face 6</item>
<item>Watch face 7</item>
<item>Watch face 8</item>
<item>Watch face 9</item>
<item>Watch face 10</item>
<item>Watch face 11</item>
</string-array>
<string-array name="pref_moyoung_watch_face_values">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
<item>10</item>
<item>11</item>
</string-array>
<string-array name="pref_moyoung_device_version">
<item>Chinese edition</item>
<item>International edition</item>
</string-array>
<string-array name="pref_moyoung_device_version_values">
<item>0</item>
<item>1</item>
</string-array>
<string-array name="prefs_heartrate_measurement_interval">
<!-- This will be filled dynamically by HeartRateCapability -->
<item name="0">@string/off</item>

View File

@ -436,6 +436,8 @@
<string name="pref_display_add_device_fab_off">Visible only if no device is added</string>
<string name="pref_title_huami_force_new_protocol">New Auth Protocol</string>
<string name="pref_summary_huami_force_new_protocol">Enable if your device no longer connects after a firmware upgrade</string>
<string name="pref_watch_face">Watch face</string>
<string name="pref_device_version">Device version</string>
<!-- HPlus Preferences -->
<string name="pref_title_unit_system">Units</string>
<string name="pref_title_timeformat">Time format</string>
@ -702,6 +704,8 @@
<string name="busy_task_fetch_sleep_respiratory_rate_data">Fetching sleep respiratory rate data</string>
<string name="busy_task_fetch_temperature">Fetching temperature data</string>
<string name="busy_task_fetch_statistics">Fetching statistics</string>
<string name="busy_task_fetch_training_data">Fetching last training data</string>
<string name="busy_task_fetch_training_data_finished">Training data recieved!</string>
<string name="sleep_activity_date_range">From %1$s to %2$s</string>
<string name="prefs_wearside">Wearing left or right?</string>
<string name="prefs_weardirection">Wearing direction</string>
@ -742,6 +746,7 @@
<string name="interval_five_minutes">every 5 minutes</string>
<string name="interval_ten_minutes">every 10 minutes</string>
<string name="interval_fifteen_minutes">every 15 minutes</string>
<string name="interval_twenty_minutes">every 20 minutes</string>
<string name="interval_thirty_minutes">every 30 minutes</string>
<string name="interval_forty_five_minutes">every 45 minutes</string>
<string name="heartrate_bpm_40">40 bpm</string>
@ -888,6 +893,10 @@
<string name="activity_list_summary_activities">Activities</string>
<string name="dialog_hide">Hide</string>
<string name="show_ongoing_activity">Show ongoing activity popup</string>
<string name="charts_show_balance_sleep_title">Show sleep balance</string>
<string name="charts_show_balance_sleep_summary">Show excess / lack of sleep in weekly and monthly charts</string>
<string name="charts_show_balance_steps_title">Show steps balance</string>
<string name="charts_show_balance_steps_summary">Show excess / lack of steps in weekly and monthly charts</string>
<string name="lack_of_sleep">Lack of sleep: %1$s</string>
<string name="overslept">Overslept: %1$s</string>
<string name="prefs_sounds">Sounds</string>
@ -1770,6 +1779,7 @@
<string name="devicetype_garmin_instinct_2_solar">Garmin Instinct 2 Solar</string>
<string name="devicetype_garmin_instinct_2_soltac">Garmin Instinct 2 SolTac</string>
<string name="devicetype_garmin_instinct_crossover">Garmin Instinct Crossover</string>
<string name="devicetype_garmin_forerunner_45">Garmin Forerunner 45</string>
<string name="devicetype_garmin_forerunner_55">Garmin Forerunner 55</string>
<string name="devicetype_garmin_forerunner_165">Garmin Forerunner 165</string>
<string name="devicetype_garmin_forerunner_235">Garmin Forerunner 235</string>
@ -1907,6 +1917,8 @@
<string name="devicetype_colmi_r06">Colmi R06</string>
<string name="devicetype_colmi_r09">Colmi R09</string>
<string name="devicetype_colmi_r10">Colmi R10</string>
<string name="devicetype_colmi_i28_ultra">Colmi i28 Ultra</string>
<string name="devicetype_misirun_c17">Misirun C17</string>
<string name="devicetype_bandw_pseries">Bowers and Wilkins P series</string>
<string name="choose_auto_export_location">Choose export location</string>
<string name="notification_channel_name">General</string>
@ -3579,4 +3591,7 @@
<string name="battery_minimum_charge">Minimum battery charge in %</string>
<string name="battery_allow_pass_though_summary">When enabled, the battery can be charged while discharging</string>
<string name="battery_allow_pass_through">Allow battery pass-through</string>
<string name="canned_replies_not_empty">There should be at least one canned reply.</string>
<string name="permission_disable_doze_title">Ignore battery optimizations</string>
<string name="permission_disable_doze_summary">Allows running in the background unhindered by Android\'s battery optimizations</string>
</resources>

View File

@ -78,6 +78,22 @@
android:title="@string/pref_title_charts_range"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="charts_show_balance_sleep"
android:layout="@layout/preference_checkbox"
android:summary="@string/charts_show_balance_sleep_summary"
android:title="@string/charts_show_balance_sleep_title"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="charts_show_balance_steps"
android:layout="@layout/preference_checkbox"
android:summary="@string/charts_show_balance_steps_summary"
android:title="@string/charts_show_balance_steps_title"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="charts_show_ongoing_activity"

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_block"
android:key="do_not_disturb_on_off"
android:layout="@layout/preference_checkbox"
android:title="@string/mi2_prefs_do_not_disturb"
android:summary="@string/mi2_prefs_do_not_disturb_summary" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_action_find_lost_device"
android:key="do_not_disturb_follow_phone"
android:layout="@layout/preference_checkbox"
android:title="@string/pref_dnd_follow_phone_title"
android:summary="@string/pref_dnd_follow_phone_summary" />
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:defaultValue="0"
android:entries="@array/prefs_heartrate_measurement_interval"
android:entryValues="@array/prefs_heartrate_measurement_interval_values"
android:icon="@drawable/ic_heartrate"
android:key="heartrate_measurement_interval"
android:summary="%s"
android:title="@string/prefs_title_heartrate_measurement_interval" />
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:icon="@drawable/ic_chair"
android:key="screen_inactivity"
android:persistent="false"
android:summary="@string/mi2_prefs_inactivity_warnings_summary"
android:title="@string/mi2_prefs_inactivity_warnings">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="inactivity_warnings_enable"
android:layout="@layout/preference_checkbox"
android:title="@string/mi2_prefs_inactivity_warnings"
android:summary="@string/mi2_prefs_inactivity_warnings_summary" />
<EditTextPreference
android:defaultValue="60"
android:dependency="inactivity_warnings_enable"
android:key="inactivity_warnings_threshold"
android:summary="@string/mi2_prefs_inactivity_warnings_summary"
android:title="@string/mi2_prefs_inactivity_warnings_threshold"/>
<EditTextPreference
android:defaultValue="60"
android:dependency="inactivity_warnings_enable"
android:key="inactivity_warnings_steps"
android:title="@string/inactivity_warnings_minimum_steps_title"
android:summary="@string/inactivity_warnings_minimum_steps_summary"/>
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
android:defaultValue="06:00"
android:dependency="inactivity_warnings_enable"
android:key="inactivity_warnings_start"
android:title="@string/mi2_prefs_do_not_disturb_start" />
<nodomain.freeyourgadget.gadgetbridge.util.XTimePreference
android:defaultValue="23:00"
android:dependency="inactivity_warnings_enable"
android:key="inactivity_warnings_end"
android:title="@string/mi2_prefs_do_not_disturb_end" />
</PreferenceScreen>
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:defaultValue="1"
android:entries="@array/pref_moyoung_device_version"
android:entryValues="@array/pref_moyoung_device_version_values"
android:key="moyoung_device_version"
android:summary="%s"
android:title="@string/pref_device_version"
android:icon="@drawable/ic_language" />
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:icon="@drawable/ic_widgets"
android:defaultValue="1"
android:entries="@array/pref_moyoung_watch_face"
android:entryValues="@array/pref_moyoung_watch_face_values"
android:key="moyoung_watch_face"
android:summary="%s"
android:title="@string/pref_watch_face" />
</androidx.preference.PreferenceScreen>

View File

@ -22,6 +22,7 @@ import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCapabilities;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
@ -44,7 +45,13 @@ public class MockSonyCoordinator extends SonyHeadphonesCoordinator {
capabilities.add(capability);
}
@Override
public Set<SonyHeadphonesCapabilities> getCapabilities() {
return capabilities;
}
@Override
public Set<SonyHeadphonesCapabilities> getCapabilities(final GBDevice device) {
return getCapabilities();
}
}

View File

@ -24,6 +24,7 @@ import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphon
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.assertRequests;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.handleMessage;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@ -35,6 +36,7 @@ import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCapabilities;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM4Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControlButtonMode;
@ -48,16 +50,35 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SpeakT
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SpeakToChatEnabled;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.VoiceNotifications;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.MockSonyCoordinator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params.BatteryType;
public class SonyProtocolImplV2Test {
private final MockSonyCoordinator coordinator = new MockSonyCoordinator();
private final SonyProtocolImplV2 protocol = new SonyProtocolImplV2(null) {
@Override
protected SonyHeadphonesCoordinator getCoordinator() {
return new SonyWF1000XM4Coordinator();
return coordinator;
}
};
@Before
public void before() {
coordinator.getCapabilities().clear();
// Same as the WF-1000XM4
coordinator.getCapabilities().addAll(Arrays.asList(
SonyHeadphonesCapabilities.BatteryDual,
SonyHeadphonesCapabilities.BatteryCase,
SonyHeadphonesCapabilities.AmbientSoundControl,
SonyHeadphonesCapabilities.WindNoiseReduction,
SonyHeadphonesCapabilities.EqualizerSimple,
SonyHeadphonesCapabilities.AudioUpsampling,
SonyHeadphonesCapabilities.ButtonModesLeftRight,
SonyHeadphonesCapabilities.PauseWhenTakenOff,
SonyHeadphonesCapabilities.AutomaticPowerOffWhenTakenOff
));
}
@Test
public void getAmbientSoundControl() {
// TODO