mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-23 02:16:48 +01:00
Compare commits
66 Commits
e302318d28
...
c65f7bc19c
Author | SHA1 | Date | |
---|---|---|---|
|
c65f7bc19c | ||
|
822e67c15d | ||
|
7ef3473f40 | ||
|
5c7d8c8fa8 | ||
|
504faf6db0 | ||
|
a586a9af52 | ||
|
18768f5df3 | ||
|
7db655cd39 | ||
|
4106e7b3cd | ||
|
cb2b216dde | ||
|
b5b4727564 | ||
|
e95892f30b | ||
|
0f9c278a2e | ||
|
ab882a845a | ||
|
c1ef7c6a46 | ||
|
61ad31aa80 | ||
|
1ad3500453 | ||
|
9b463a8bd1 | ||
|
185b554980 | ||
|
7003bb386a | ||
|
a21ce4eeb3 | ||
|
c93dc5bb09 | ||
|
40abeea54a | ||
|
125b493c22 | ||
|
d8992e104c | ||
|
fb3ac9316a | ||
|
5e411b8f81 | ||
|
5fb78514a8 | ||
|
57cbe69138 | ||
|
6bf4a46f35 | ||
|
6a41f19e9e | ||
|
56becb2ea9 | ||
|
ea58fd3f0d | ||
|
5b0c2a9b69 | ||
|
2f409f8b0a | ||
|
62cc891212 | ||
|
f6b18ff680 | ||
|
6f8424e5d7 | ||
|
3aa1f7bec5 | ||
|
16aed1364b | ||
|
212289645f | ||
|
6b5c5ae0ac | ||
|
9d1a57b6c2 | ||
|
b56ed974a3 | ||
|
b5bd4da9b1 | ||
|
1d2404a4e6 | ||
|
39e7bd8c62 | ||
|
5f91715c89 | ||
|
1618fda418 | ||
|
e453855e88 | ||
|
dc1533b4ed | ||
|
1a3a7dec05 | ||
|
87bc2e6ed7 | ||
|
9bd828814e | ||
|
6aa7280967 | ||
|
f16e2eeabb | ||
|
e83555f099 | ||
|
9b6fce566d | ||
|
de37e5b6fd | ||
|
cbb710abe7 | ||
|
31b8fd683d | ||
|
82f221752e | ||
|
c2c1e48c85 | ||
|
810df3055c | ||
|
a72de07d2a | ||
|
7a0e43a4de |
@ -124,6 +124,7 @@
|
||||
<w>protomors</w>
|
||||
<w>qhybrid</w>
|
||||
<w>quallenauge</w>
|
||||
<w>realme</w>
|
||||
<w>rebelo</w>
|
||||
<w>roidmi</w>
|
||||
<w>romanization</w>
|
||||
|
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,5 +1,20 @@
|
||||
### Changelog
|
||||
|
||||
#### Next Release (WIP)
|
||||
* Initial support for Bowers and Wilkins P Series
|
||||
* Initial support for Garmin Fenix 6S Pro, Forerunner 55/235/620
|
||||
* Initial support for Huawei Band 3 Pro
|
||||
* Initial support for Oppo Enco Air
|
||||
* Huawei: Display high-resolution heart rate
|
||||
* Huawei: Improve activity parsing
|
||||
* Huawei: Sync skin temperature
|
||||
|
||||
#### 0.82.1
|
||||
* Huawei: Improve activity parsing
|
||||
* Huawei Watch GT: Fix connection failure
|
||||
* Withings: Fix crash on connection
|
||||
* Improve Armenian transliterator for mixed-case words
|
||||
|
||||
#### 0.82.0
|
||||
* Initial support for Anker Soundcore Liberty 4 NC
|
||||
* Initial support for CMF Buds Pro 2 / Watch Pro 2
|
||||
|
@ -49,6 +49,8 @@ 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";
|
||||
|
||||
@ -145,6 +147,10 @@ public class GBDaoGenerator {
|
||||
addColmiSleepStageSample(schema, user, device);
|
||||
addColmiHrvValueSample(schema, user, device);
|
||||
addColmiHrvSummarySample(schema, user, device);
|
||||
addMoyoungActivitySample(schema, user, device);
|
||||
addMoyoungHeartRateSample(schema, user, device);
|
||||
addMoyoungSpo2Sample(schema, user, device);
|
||||
addMoyoungBloodPressureSample(schema, user, device);
|
||||
|
||||
addHuaweiActivitySample(schema, user, device);
|
||||
|
||||
@ -584,6 +590,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);
|
||||
@ -1006,6 +1017,41 @@ 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 void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
|
||||
activitySample.setSuperclass(superClass);
|
||||
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");
|
||||
|
@ -79,8 +79,8 @@ android {
|
||||
minSdkVersion 21
|
||||
|
||||
// Note: always bump BOTH versionCode and versionName!
|
||||
versionName "0.82.0"
|
||||
versionCode 233
|
||||
versionName "0.82.1"
|
||||
versionCode 234
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
buildConfigField "String", "GIT_HASH_SHORT", "\"${getGitHashShort()}\""
|
||||
buildConfigField "boolean", "INTERNET_ACCESS", "false"
|
||||
@ -198,7 +198,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.3'
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation "androidx.camera:camera-core:1.4.0"
|
||||
|
@ -199,6 +199,10 @@
|
||||
android:label="@string/title_activity_appmanager"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.musicmanager.MusicManagerActivity"
|
||||
android:label="@string/title_activity_musicmanager"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.AppBlacklistActivity"
|
||||
android:label="@string/title_activity_notification_management"
|
||||
|
@ -97,14 +97,14 @@ public class BodyEnergyFragment extends AbstractChartFragment<BodyEnergyFragment
|
||||
|
||||
@Override
|
||||
protected BodyEnergyData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
List<? extends BodyEnergySample> samples = getBodyEnergySamples(db, device, getTSStart(), getTSEnd());
|
||||
return new BodyEnergyData(samples);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(BodyEnergyData bodyEnergyData) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
List<Entry> lineEntries = new ArrayList<>();
|
||||
final List<ILineDataSet> lineDataSets = new ArrayList<>();
|
||||
|
@ -126,10 +126,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
|
||||
@Override
|
||||
protected HRVStatusWeeklyData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
Date tsEnd = getChartsHost().getEndDate();
|
||||
day.setTime(tsEnd);
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(tsEnd);
|
||||
mDateView.setText(formattedDate);
|
||||
day.setTime(getEndDate());
|
||||
|
||||
List<HRVStatusDayData> weeklyData = getWeeklyData(db, day, device);
|
||||
return new HRVStatusWeeklyData(weeklyData);
|
||||
}
|
||||
@ -164,6 +162,9 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(HRVStatusWeeklyData weeklyData) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
mWeeklyHRVStatusChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
|
||||
List<Entry> lineEntries = new ArrayList<>();
|
||||
final List<ILineDataSet> lineDataSets = new ArrayList<>();
|
||||
|
@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Accumulator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDailyFragment.HeartRateData> {
|
||||
@ -123,9 +124,7 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
day.add(Calendar.HOUR, 0);
|
||||
int startTs = (int) (day.getTimeInMillis() / 1000);
|
||||
int endTs = startTs + 24 * 60 * 60 - 1;
|
||||
Date date = new Date((long) endTs * 1000);
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(date);
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
List<? extends ActivitySample> samples = getActivitySamples(db, device, startTs, endTs);
|
||||
|
||||
int restingHeartRate = -1;
|
||||
@ -211,20 +210,33 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(HeartRateDailyFragment.HeartRateData data) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTime(getEndDate());
|
||||
day.add(Calendar.DATE, 0);
|
||||
day.set(Calendar.HOUR_OF_DAY, 0);
|
||||
day.set(Calendar.MINUTE, 0);
|
||||
day.set(Calendar.SECOND, 0);
|
||||
day.add(Calendar.HOUR, 0);
|
||||
int startTs = (int) (day.getTimeInMillis() / 1000);
|
||||
int endTs = startTs + 24 * 60 * 60 - 1;
|
||||
Date date = new Date((long) endTs * 1000);
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(date);
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
|
||||
final TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
final List<Entry> lineEntries = new ArrayList<>();
|
||||
List<? extends ActivitySample> samples = data.samples;
|
||||
int average = 0;
|
||||
int minimum = 0;
|
||||
int maximum = 0;
|
||||
int sum = 0;
|
||||
int n = 0;
|
||||
final Accumulator accumulator = new Accumulator();
|
||||
|
||||
int lastHrSampleIndex = -1;
|
||||
for (int i =0; i < samples.size(); i++) {
|
||||
ActivitySample sample = samples.get(i);
|
||||
int ts = tsTranslation.shorten(sample.getTimestamp());
|
||||
if (sample.getKind() != ActivityKind.NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
|
||||
final ActivitySample sample = samples.get(i);
|
||||
final int ts = tsTranslation.shorten(sample.getTimestamp());
|
||||
if (!heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
|
||||
continue;
|
||||
}
|
||||
if (sample.getKind() != ActivityKind.NOT_WORN) {
|
||||
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
|
||||
lineEntries.add(new Entry(lastHrSampleIndex + 1, 0 ));
|
||||
lineEntries.add(new Entry(ts - 1, 0));
|
||||
@ -232,17 +244,7 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
lineEntries.add(new Entry(ts, sample.getHeartRate()));
|
||||
lastHrSampleIndex = ts;
|
||||
}
|
||||
if (sample.getHeartRate() <= 0) {
|
||||
continue;
|
||||
}
|
||||
n++;
|
||||
sum += sample.getHeartRate();
|
||||
if (sample.getHeartRate() > maximum) {
|
||||
maximum = sample.getHeartRate();
|
||||
}
|
||||
if (minimum == 0 || sample.getHeartRate() < minimum) {
|
||||
minimum = sample.getHeartRate();
|
||||
}
|
||||
accumulator.add(sample.getHeartRate());
|
||||
}
|
||||
|
||||
LineDataSet dataSet = new LineDataSet(lineEntries, "Heart Rate");
|
||||
@ -255,16 +257,15 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
dataSet.setColor(HEARTRATE_COLOR);
|
||||
dataSet.setValueTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
if (n > 0 && sum > 0) {
|
||||
average = sum / n;
|
||||
}
|
||||
final int average = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getAverage()) : -1;
|
||||
final int minimum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMin()) : -1;
|
||||
final int maximum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMax()) : -1;
|
||||
|
||||
hrAverage.setText(average > 0 ? getString(R.string.bpm_value_unit, average) : "-");
|
||||
hrMinimum.setText(minimum > 0 ? getString(R.string.bpm_value_unit, minimum) : "-");
|
||||
hrMaximum.setText(maximum > 0 ? getString(R.string.bpm_value_unit, maximum) : "-");
|
||||
hrResting.setText(data.restingHeartRate > 0 ? getString(R.string.bpm_value_unit, data.restingHeartRate) : "-");
|
||||
|
||||
|
||||
if (minimum > 0) {
|
||||
hrLineChart.getAxisLeft().setAxisMinimum(Math.max(minimum - 30, 0));
|
||||
hrLineChart.getAxisRight().setAxisMinimum(Math.max(minimum - 30, 0));
|
||||
@ -279,7 +280,7 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
|
||||
hrLineChart.getAxisLeft().removeAllLimitLines();
|
||||
|
||||
if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
|
||||
if (average > 0 && GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
|
||||
final LimitLine averageLine = new LimitLine(average);
|
||||
averageLine.setLineWidth(1.5f);
|
||||
averageLine.enableDashedLine(15f, 10f, 0f);
|
||||
|
@ -37,6 +37,7 @@ import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
@ -101,8 +102,6 @@ public class StepsDailyFragment extends StepsFragment<StepsDailyFragment.StepsDa
|
||||
protected StepsDailyFragment.StepsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTime(chartsHost.getEndDate());
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(chartsHost.getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
List<StepsDay> stepsDayList = getMyStepsDaysData(db, day, device);
|
||||
final StepsDay stepsDay;
|
||||
if (stepsDayList.isEmpty()) {
|
||||
@ -117,6 +116,9 @@ public class StepsDailyFragment extends StepsFragment<StepsDailyFragment.StepsDa
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(StepsDailyFragment.StepsData stepsData) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
final int width = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
300,
|
||||
@ -132,7 +134,9 @@ public class StepsDailyFragment extends StepsFragment<StepsDailyFragment.StepsDa
|
||||
));
|
||||
|
||||
steps.setText(String.format(String.valueOf(stepsData.todayStepsDay.steps)));
|
||||
distance.setText(getString(R.string.steps_distance_unit, stepsData.todayStepsDay.distance));
|
||||
|
||||
final WorkoutValueFormatter valueFormatter = new WorkoutValueFormatter();
|
||||
distance.setText(valueFormatter.formatValue(stepsData.todayStepsDay.distance, "km"));
|
||||
|
||||
// Chart
|
||||
final List<LegendEntry> legendEntries = new ArrayList<>(1);
|
||||
|
@ -31,6 +31,7 @@ import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
@ -142,18 +143,19 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
@Override
|
||||
protected StepsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
Date to = new Date((long) this.getTSEnd() * 1000);
|
||||
Date from = DateUtils.addDays(to,-(TOTAL_DAYS - 1));
|
||||
String toFormattedDate = new SimpleDateFormat("E, MMM dd").format(to);
|
||||
String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from);
|
||||
mDateView.setText(fromFormattedDate + " - " + toFormattedDate);
|
||||
day.setTime(to);
|
||||
day.setTime(getEndDate());
|
||||
List<StepsDay> stepsDaysData = getMyStepsDaysData(db, day, device);
|
||||
return new StepsData(stepsDaysData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(StepsData stepsData) {
|
||||
Date to = new Date((long) getTSEnd() * 1000);
|
||||
Date from = DateUtils.addDays(to,-(TOTAL_DAYS - 1));
|
||||
String toFormattedDate = new SimpleDateFormat("E, MMM dd").format(to);
|
||||
String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from);
|
||||
mDateView.setText(fromFormattedDate + " - " + toFormattedDate);
|
||||
|
||||
stepsChart.setData(null);
|
||||
|
||||
List<BarEntry> entries = new ArrayList<>();
|
||||
@ -177,9 +179,10 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
}
|
||||
stepsChart.setData(barData);
|
||||
stepsAvg.setText(String.format(String.valueOf(stepsData.stepsDailyAvg)));
|
||||
distanceAvg.setText(getString(R.string.steps_distance_unit, stepsData.distanceDailyAvg));
|
||||
final WorkoutValueFormatter valueFormatter = new WorkoutValueFormatter();
|
||||
distanceAvg.setText(valueFormatter.formatValue(stepsData.distanceDailyAvg, "km"));
|
||||
stepsTotal.setText(String.format(String.valueOf(stepsData.totalSteps)));
|
||||
distanceTotal.setText(getString(R.string.steps_distance_unit, stepsData.totalDistance));
|
||||
distanceTotal.setText(valueFormatter.formatValue(stepsData.totalDistance, "km"));
|
||||
}
|
||||
|
||||
ValueFormatter getStepsChartDayValueFormatter(StepsPeriodFragment.StepsData stepsData) {
|
||||
|
@ -118,8 +118,6 @@ public class VO2MaxFragment extends AbstractChartFragment<VO2MaxFragment.VO2MaxD
|
||||
|
||||
@Override
|
||||
protected VO2MaxData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
List<VO2MaxRecord> records = new ArrayList<>();
|
||||
int tsEnd = getTSEnd();
|
||||
Calendar day = Calendar.getInstance();
|
||||
@ -145,7 +143,9 @@ public class VO2MaxFragment extends AbstractChartFragment<VO2MaxFragment.VO2MaxD
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(VO2MaxData vo2MaxData) {
|
||||
TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
List<Entry> runningEntries = new ArrayList<>();
|
||||
List<Entry> cyclingEntries = new ArrayList<>();
|
||||
vo2MaxData.records.forEach((record) -> {
|
||||
|
@ -107,6 +107,7 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_DEVICE_INTENTS = "device_intents";
|
||||
|
||||
public static final String PREF_ACTIVE_NOISE_CANCELLING_TOGGLE = "active_noise_cancelling_toggle";
|
||||
public static final String PREF_WEAR_SENSOR_TOGGLE = "wear_sensor_toggle";
|
||||
public static final String PREF_BANDW_PSERIES_VPT_ENABLED = "bandw_pseries_vpt_enabled";
|
||||
public static final String PREF_BANDW_PSERIES_VPT_LEVEL = "bandw_pseries_vpt_level";
|
||||
public static final String PREF_BANDW_PSERIES_GUI_VPT_LEVEL = "bandw_pseries_gui_vpt_level";
|
||||
@ -191,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";
|
||||
@ -224,6 +226,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";
|
||||
@ -284,6 +288,8 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_CONTACTS = "pref_contacts";
|
||||
public static final String PREF_WIDGETS = "pref_widgets";
|
||||
|
||||
public static final String PREF_MUSIC_MANAGEMENT = "pref_music_management";
|
||||
|
||||
public static final String PREF_ANTILOST_ENABLED = "pref_antilost_enabled";
|
||||
public static final String PREF_HYDRATION_SWITCH = "pref_hydration_switch";
|
||||
public static final String PREF_HYDRATION_PERIOD = "pref_hydration_period";
|
||||
|
@ -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;
|
||||
@ -70,6 +72,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureWorldClocks;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.app_specific_notifications.AppSpecificNotificationSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.musicmanager.MusicManagerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.widgets.WidgetScreensListActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
@ -568,6 +571,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);
|
||||
@ -612,6 +617,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
addPreferenceHandlerFor(PREF_SLEEP_MODE_SMART_ENABLE);
|
||||
|
||||
addPreferenceHandlerFor(PREF_ACTIVE_NOISE_CANCELLING_TOGGLE);
|
||||
addPreferenceHandlerFor(PREF_WEAR_SENSOR_TOGGLE);
|
||||
addPreferenceHandlerFor(PREF_BANDW_PSERIES_GUI_VPT_LEVEL);
|
||||
|
||||
addPreferenceHandlerFor(PREF_HYBRID_HR_DRAW_WIDGET_CIRCLES);
|
||||
@ -775,6 +781,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);
|
||||
@ -1050,6 +1059,19 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
});
|
||||
}
|
||||
|
||||
final Preference music_management = findPreference(PREF_MUSIC_MANAGEMENT);
|
||||
if (music_management != null) {
|
||||
music_management.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
final Intent intent = new Intent(getContext(), MusicManagerActivity.class);
|
||||
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final Preference widgets = findPreference(PREF_WIDGETS);
|
||||
if (widgets != null) {
|
||||
widgets.setOnPreferenceClickListener(preference -> {
|
||||
|
@ -0,0 +1,632 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.musicmanager;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.MusicListAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusicPlaylist;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GridAutoFitLayoutManager;
|
||||
|
||||
public class MusicManagerActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MusicManagerActivity.class);
|
||||
|
||||
public static final String ACTION_MUSIC_DATA
|
||||
= "nodomain.freeyourgadget.gadgetbridge.musicmanager.action.music_data";
|
||||
public static final String ACTION_MUSIC_UPDATE
|
||||
= "nodomain.freeyourgadget.gadgetbridge.musicmanager.action.music_update";
|
||||
|
||||
protected GBDevice mGBDevice = null;
|
||||
|
||||
private View loadingView = null;
|
||||
private TextView musicDeviceInfo = null;
|
||||
|
||||
private final List<GBDeviceMusic> allMusic = new ArrayList<>();
|
||||
|
||||
private final List<GBDeviceMusic> musicList = new ArrayList<>();
|
||||
private MusicListAdapter musicAdapter;
|
||||
|
||||
private final List<GBDeviceMusicPlaylist> playlists = new ArrayList<>();
|
||||
private ArrayAdapter<GBDeviceMusicPlaylist> playlistAdapter;
|
||||
|
||||
private View playlistSpinnerLayout;
|
||||
private Spinner playlistsSpinner;
|
||||
|
||||
private FloatingActionButton fabMusicUpload;
|
||||
private FloatingActionButton fabMusicPlaylistAdd;
|
||||
|
||||
private int maxMusicCount = 0;
|
||||
private int maxPlaylistCount = 0;
|
||||
|
||||
public GBDevice getGBDevice() {
|
||||
return mGBDevice;
|
||||
}
|
||||
|
||||
Handler loadingTimeout = new Handler();
|
||||
Runnable loadingRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GB.toast(getString(R.string.music_error), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
stopLoading();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_musicmanager);
|
||||
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
}
|
||||
if (mGBDevice == null) {
|
||||
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||||
}
|
||||
|
||||
fabMusicUpload = findViewById(R.id.fab_music_upload);
|
||||
assert fabMusicUpload != null;
|
||||
fabMusicUpload.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("audio/*");
|
||||
openAudioActivityResultLauncher.launch(intent);
|
||||
}
|
||||
});
|
||||
|
||||
fabMusicPlaylistAdd = findViewById(R.id.fab_music_playlist_add);
|
||||
assert fabMusicPlaylistAdd != null;
|
||||
fabMusicPlaylistAdd.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
addMusicPlaylist();
|
||||
}
|
||||
});
|
||||
|
||||
hideActionButtons();
|
||||
|
||||
RecyclerView musicListView = findViewById(R.id.music_songs_list);
|
||||
loadingView = findViewById(R.id.music_loading);
|
||||
|
||||
musicDeviceInfo = findViewById(R.id.music_device_info);
|
||||
|
||||
musicListView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy > 0) {
|
||||
hideActionButtons();
|
||||
} else if (dy < 0) {
|
||||
showActionButtons();
|
||||
}
|
||||
}
|
||||
});
|
||||
musicListView.setLayoutManager(new GridAutoFitLayoutManager(this, 300));
|
||||
|
||||
musicAdapter = new MusicListAdapter(
|
||||
musicList,
|
||||
new MusicListAdapter.onItemAction() {
|
||||
@Override
|
||||
public void onItemClick(View view, GBDeviceMusic music) {
|
||||
openPopupMenu(view, music);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClick(View view, GBDeviceMusic music) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
musicListView.setAdapter(musicAdapter);
|
||||
|
||||
playlistSpinnerLayout = findViewById(R.id.music_playlists_layout);
|
||||
|
||||
playlistsSpinner = findViewById(R.id.music_playlists);
|
||||
|
||||
ImageButton renamePlaylist = findViewById(R.id.music_playlist_rename);
|
||||
assert renamePlaylist != null;
|
||||
renamePlaylist.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
renameMusicPlaylist((GBDeviceMusicPlaylist) playlistsSpinner.getSelectedItem());
|
||||
}
|
||||
});
|
||||
|
||||
ImageButton deletePlaylist = findViewById(R.id.music_playlist_delete);
|
||||
assert deletePlaylist != null;
|
||||
deletePlaylist.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
deleteMusicPlaylist((GBDeviceMusicPlaylist) playlistsSpinner.getSelectedItem());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
playlistsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
GBDeviceMusicPlaylist item = (GBDeviceMusicPlaylist) adapterView.getItemAtPosition(i);
|
||||
if (item.getId() == 0) {
|
||||
deletePlaylist.setVisibility(View.GONE);
|
||||
renamePlaylist.setVisibility(View.GONE);
|
||||
|
||||
} else {
|
||||
deletePlaylist.setVisibility(View.VISIBLE);
|
||||
renamePlaylist.setVisibility(View.VISIBLE);
|
||||
}
|
||||
updateCurrentMusicList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
playlistAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, playlists);
|
||||
initPlaylists();
|
||||
|
||||
playlistAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
playlistsSpinner.setAdapter(playlistAdapter);
|
||||
}
|
||||
|
||||
private void hideActionButtons() {
|
||||
fabMusicUpload.hide();
|
||||
fabMusicPlaylistAdd.hide();
|
||||
|
||||
}
|
||||
|
||||
private void showActionButtons() {
|
||||
fabMusicUpload.show();
|
||||
if(maxPlaylistCount > 0) {
|
||||
fabMusicPlaylistAdd.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void startLoading(long timeout) {
|
||||
hideActionButtons();
|
||||
loadingView.setVisibility(View.VISIBLE);
|
||||
if(timeout > 0) {
|
||||
loadingTimeout.postDelayed(loadingRunnable, timeout);
|
||||
}
|
||||
}
|
||||
private void startLoading() {
|
||||
startLoading(4000);
|
||||
}
|
||||
|
||||
private void stopLoading() {
|
||||
loadingTimeout.removeCallbacks(loadingRunnable);
|
||||
loadingView.setVisibility(View.GONE);
|
||||
showActionButtons();
|
||||
}
|
||||
|
||||
private void updateCurrentMusicList() {
|
||||
GBDeviceMusicPlaylist current = (GBDeviceMusicPlaylist) playlistsSpinner.getSelectedItem();
|
||||
musicList.clear();
|
||||
if (current.getId() == 0) {
|
||||
musicList.addAll(allMusic);
|
||||
} else {
|
||||
List<GBDeviceMusic> filtered = allMusic.stream().filter(m -> current.getMusicIds().contains(m.getId())).collect(Collectors.toList());
|
||||
musicList.addAll(filtered);
|
||||
}
|
||||
musicAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void initPlaylists() {
|
||||
playlists.clear();
|
||||
playlists.add(new GBDeviceMusicPlaylist(0,this.getString(R.string.music_all_songs),null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_MUSIC_DATA);
|
||||
filter.addAction(ACTION_MUSIC_UPDATE);
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
|
||||
|
||||
// Load music data without timeout
|
||||
startLoading(0);
|
||||
GBApplication.deviceService(mGBDevice).onMusicListReq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
ActivityResultLauncher<Intent> openAudioActivityResultLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
new ActivityResultCallback<ActivityResult>() {
|
||||
@Override
|
||||
public void onActivityResult(ActivityResult result) {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Intent startIntent = new Intent(MusicManagerActivity.this, FwAppInstallerActivity.class);
|
||||
startIntent.setAction(Intent.ACTION_VIEW);
|
||||
startIntent.setDataAndType(result.getData().getData(), null);
|
||||
startActivity(startIntent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
public boolean openPopupMenu(View view, GBDeviceMusic music) {
|
||||
PopupMenu popupMenu = new PopupMenu(this, view);
|
||||
popupMenu.getMenuInflater().inflate(R.menu.musicmanager_context, popupMenu.getMenu());
|
||||
Menu menu = popupMenu.getMenu();
|
||||
|
||||
if (playlists.size() <= 1) {
|
||||
menu.removeItem(R.id.musicmanager_add_to_playlist);
|
||||
}
|
||||
|
||||
GBDeviceMusicPlaylist current = (GBDeviceMusicPlaylist) playlistsSpinner.getSelectedItem();
|
||||
musicList.clear();
|
||||
if (current.getId() == 0) {
|
||||
menu.removeItem(R.id.musicmanager_delete_from_playlist);
|
||||
} else {
|
||||
menu.removeItem(R.id.musicmanager_delete);
|
||||
}
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
return onPopupItemSelected(item, music);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
popupMenu.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean onPopupItemSelected(final MenuItem item, final GBDeviceMusic music) {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == R.id.musicmanager_delete || itemId == R.id.musicmanager_delete_from_playlist) {
|
||||
deleteMusicConfirm(music);
|
||||
return true;
|
||||
} else if (itemId == R.id.musicmanager_add_to_playlist) {
|
||||
addMusicSongToPlaylist(music);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void deleteMusicConfirm(final GBDeviceMusic music) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.Delete)
|
||||
.setMessage(this.getString(R.string.music_delete_confirm_description, music.getTitle()))
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
deleteMusicFromDevice((GBDeviceMusicPlaylist) playlistsSpinner.getSelectedItem(), music);
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void addPlaylistToDevice(final String playlistName) {
|
||||
startLoading();
|
||||
GBApplication.deviceService(mGBDevice).onMusicOperation(0, -1, playlistName, null);
|
||||
}
|
||||
|
||||
private void deletePlaylistFromDevice(final GBDeviceMusicPlaylist playlist) {
|
||||
startLoading();
|
||||
GBApplication.deviceService(mGBDevice).onMusicOperation(1, playlist.getId(), null, null);
|
||||
}
|
||||
|
||||
private void renamePlaylistOnDevice(final GBDeviceMusicPlaylist playlist, String newPlaylistName) {
|
||||
startLoading();
|
||||
GBApplication.deviceService(mGBDevice).onMusicOperation(2, playlist.getId(), newPlaylistName, null);
|
||||
}
|
||||
|
||||
private void addMusicToDevicePlaylist(GBDeviceMusicPlaylist playlist, final GBDeviceMusic music) {
|
||||
startLoading();
|
||||
ArrayList<Integer> list = new ArrayList<>();
|
||||
list.add(music.getId());
|
||||
GBApplication.deviceService(mGBDevice).onMusicOperation(3, playlist.getId(), null, list);
|
||||
}
|
||||
|
||||
private void deleteMusicFromDevice(GBDeviceMusicPlaylist playlist, final GBDeviceMusic music) {
|
||||
startLoading();
|
||||
ArrayList<Integer> list = new ArrayList<>();
|
||||
list.add(music.getId());
|
||||
GBApplication.deviceService(mGBDevice).onMusicOperation(4, playlist.getId(), null, list);
|
||||
}
|
||||
|
||||
private void addMusicPlaylist() {
|
||||
final EditText input = new EditText(this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
|
||||
FrameLayout container = new FrameLayout(this);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
params.rightMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
input.setLayoutParams(params);
|
||||
container.addView(input);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.music_new_playlist)
|
||||
.setView(container)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
|
||||
String playlistName = input.getText().toString();
|
||||
addPlaylistToDevice(playlistName);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void renameMusicPlaylist(GBDeviceMusicPlaylist playlist) {
|
||||
if(playlist.getId() == 0)
|
||||
return;
|
||||
final EditText input = new EditText(this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
input.setText(playlist.getName());
|
||||
|
||||
FrameLayout container = new FrameLayout(this);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
params.rightMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
input.setLayoutParams(params);
|
||||
container.addView(input);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.music_rename_playlist)
|
||||
.setView(container)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
|
||||
String playlistName = input.getText().toString();
|
||||
renamePlaylistOnDevice(playlist, playlistName);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void deleteMusicPlaylist(GBDeviceMusicPlaylist playlist) {
|
||||
if(playlist.getId() == 0)
|
||||
return;
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.Delete)
|
||||
.setMessage(this.getString(R.string.music_delete_confirm_description, playlist.getName()))
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
deletePlaylistFromDevice(playlist);
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void addMusicSongToPlaylist(final GBDeviceMusic music) {
|
||||
final Spinner dPlaylists = new Spinner(this);
|
||||
|
||||
List<GBDeviceMusicPlaylist> dialogPlaylists = new ArrayList<>();
|
||||
for (GBDeviceMusicPlaylist playlist : playlists) {
|
||||
if (playlist.getId() != 0) {
|
||||
dialogPlaylists.add(playlist);
|
||||
}
|
||||
}
|
||||
|
||||
ArrayAdapter<GBDeviceMusicPlaylist> dialogPlaylistAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, dialogPlaylists);
|
||||
dialogPlaylistAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
dPlaylists.setAdapter(dialogPlaylistAdapter);
|
||||
|
||||
FrameLayout container = new FrameLayout(this);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
params.rightMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
dPlaylists.setLayoutParams(params);
|
||||
container.addView(dPlaylists);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.music_add_to_playlist)
|
||||
.setView(container)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
|
||||
GBDeviceMusicPlaylist playlist = (GBDeviceMusicPlaylist) dPlaylists.getSelectedItem();
|
||||
addMusicToDevicePlaylist(playlist, music);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void startSyncFromDevice(Intent intent) {
|
||||
String info = intent.getStringExtra("deviceInfo");
|
||||
if (!TextUtils.isEmpty(info)) {
|
||||
musicDeviceInfo.setText(info);
|
||||
} else {
|
||||
musicDeviceInfo.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
maxMusicCount = intent.getIntExtra("maxMusicCount", 0);
|
||||
maxPlaylistCount = intent.getIntExtra("maxPlaylistCount", 0);
|
||||
|
||||
// Hide playlist if device does not support it.
|
||||
playlistSpinnerLayout.setVisibility(maxPlaylistCount>0?View.VISIBLE:View.GONE);
|
||||
|
||||
allMusic.clear();
|
||||
musicList.clear();
|
||||
initPlaylists();
|
||||
}
|
||||
|
||||
private void musicListFromDevice(Intent intent) {
|
||||
ArrayList<GBDeviceMusic> list = (ArrayList<GBDeviceMusic>) intent.getSerializableExtra("musicList");
|
||||
if (list != null && !list.isEmpty()) {
|
||||
allMusic.addAll(list);
|
||||
}
|
||||
|
||||
ArrayList<GBDeviceMusicPlaylist> devicePlaylist = (ArrayList<GBDeviceMusicPlaylist>) intent.getSerializableExtra("musicPlaylist");
|
||||
if (devicePlaylist != null && !devicePlaylist.isEmpty()) {
|
||||
playlists.addAll(devicePlaylist);
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void musicOperationResponse(Intent intent) {
|
||||
int operation = intent.getIntExtra("operation", -1);
|
||||
if (operation == 0) {
|
||||
int playlistIndex = intent.getIntExtra("playlistIndex", -1);
|
||||
String playlistName = intent.getStringExtra("playlistName");
|
||||
|
||||
if (playlistIndex != -1 && !TextUtils.isEmpty(playlistName)) {
|
||||
playlists.add(new GBDeviceMusicPlaylist(playlistIndex, playlistName, new ArrayList<>()));
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (operation == 1) {
|
||||
int playlistIndex = intent.getIntExtra("playlistIndex", -1);
|
||||
if (playlistIndex != -1) {
|
||||
for (Iterator<GBDeviceMusicPlaylist> iterator = playlists.iterator(); iterator.hasNext(); ) {
|
||||
GBDeviceMusicPlaylist playlist = iterator.next();
|
||||
if (playlist.getId() == playlistIndex) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (operation == 2) {
|
||||
int playlistIndex = intent.getIntExtra("playlistIndex", -1);
|
||||
String playlistName = intent.getStringExtra("playlistName");
|
||||
if (playlistIndex != -1 && !TextUtils.isEmpty(playlistName)) {
|
||||
for (GBDeviceMusicPlaylist playlist : playlists) {
|
||||
if (playlist.getId() == playlistIndex) {
|
||||
playlist.setName(playlistName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (operation == 3) {
|
||||
int playlistIndex = intent.getIntExtra("playlistIndex", -1);
|
||||
ArrayList<Integer> ids = (ArrayList<Integer>) intent.getSerializableExtra("musicIds");
|
||||
if (playlistIndex != -1 && ids != null && !ids.isEmpty()) {
|
||||
for (GBDeviceMusicPlaylist playlist : playlists) {
|
||||
if (playlist.getId() == playlistIndex) {
|
||||
ArrayList<Integer> currentList = playlist.getMusicIds();
|
||||
for (Integer id : ids) {
|
||||
if (!currentList.contains(id))
|
||||
currentList.add(id);
|
||||
}
|
||||
playlist.setMusicIds(currentList);
|
||||
break;
|
||||
}
|
||||
}
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
updateCurrentMusicList();
|
||||
}
|
||||
|
||||
} else if (operation == 4) {
|
||||
ArrayList<Integer> ids = (ArrayList<Integer>) intent.getSerializableExtra("musicIds");
|
||||
int playlistIndex = intent.getIntExtra("playlistIndex", 0);
|
||||
if (ids != null && !ids.isEmpty()) {
|
||||
if (playlistIndex == 0) {
|
||||
for (Iterator<GBDeviceMusic> iterator = musicList.iterator(); iterator.hasNext(); ) {
|
||||
GBDeviceMusic music = iterator.next();
|
||||
if (ids.contains(music.getId())) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
for (Iterator<GBDeviceMusic> iterator = allMusic.iterator(); iterator.hasNext(); ) {
|
||||
GBDeviceMusic music = iterator.next();
|
||||
if (ids.contains(music.getId())) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (GBDeviceMusicPlaylist playlist : playlists) {
|
||||
if (playlist.getId() == playlistIndex) {
|
||||
ArrayList<Integer> currentList = playlist.getMusicIds();
|
||||
for (Integer id : ids) {
|
||||
currentList.remove(id);
|
||||
}
|
||||
playlist.setMusicIds(currentList);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
updateCurrentMusicList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
switch (action) {
|
||||
case ACTION_MUSIC_DATA: {
|
||||
if (!intent.hasExtra("type"))
|
||||
break;
|
||||
int type = intent.getIntExtra("type", -1);
|
||||
|
||||
LOG.info("UPDATE type: {}", type);
|
||||
if (type == 1) {
|
||||
startSyncFromDevice(intent);
|
||||
} else if (type == 2) {
|
||||
LOG.info("got music list or playlist from device");
|
||||
musicListFromDevice(intent);
|
||||
} else if (type == 10) {
|
||||
updateCurrentMusicList();
|
||||
stopLoading();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ACTION_MUSIC_UPDATE: {
|
||||
boolean success = intent.getBooleanExtra("success", false);
|
||||
if (intent.hasExtra("operation") && success) {
|
||||
musicOperationResponse(intent);
|
||||
}
|
||||
stopLoading();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities.workouts;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_CM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KILOMETERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS_PER_SECOND;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM;
|
||||
@ -106,6 +107,12 @@ public class WorkoutValueFormatter {
|
||||
unit = "minutes_km";
|
||||
}
|
||||
break;
|
||||
case UNIT_KILOMETERS:
|
||||
if (units.equals(UNIT_IMPERIAL)) {
|
||||
value = value * 0.621371D;
|
||||
unit = "mi";
|
||||
}
|
||||
break;
|
||||
case UNIT_METERS:
|
||||
if (units.equals(UNIT_IMPERIAL)) {
|
||||
value = value * 3.28084D;
|
||||
|
@ -0,0 +1,85 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
|
||||
public class MusicListAdapter extends RecyclerView.Adapter<MusicListAdapter.MusicViewHolder> {
|
||||
|
||||
public interface onItemAction {
|
||||
void onItemClick(View view, GBDeviceMusic music);
|
||||
boolean onItemLongClick(View view, GBDeviceMusic music);
|
||||
}
|
||||
|
||||
private final List<GBDeviceMusic> musicList;
|
||||
private final onItemAction callback;
|
||||
|
||||
public MusicListAdapter(List<GBDeviceMusic> list, onItemAction callback) {
|
||||
this.musicList = list;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return musicList.get(position).getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return musicList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MusicListAdapter.MusicViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_musicmanager_song, parent, false);
|
||||
return new MusicViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final MusicListAdapter.MusicViewHolder holder, int position) {
|
||||
final GBDeviceMusic music = musicList.get(position);
|
||||
|
||||
holder.musicTitle.setText(music.getTitle());
|
||||
holder.musicArtist.setText(music.getArtist());
|
||||
|
||||
if(callback != null) {
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
callback.onItemClick(view, music);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
return callback.onItemLongClick(view, music);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class MusicViewHolder extends RecyclerView.ViewHolder {
|
||||
final TextView musicArtist;
|
||||
final TextView musicTitle;
|
||||
|
||||
MusicViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
musicArtist = itemView.findViewById(R.id.item_details);
|
||||
musicTitle = itemView.findViewById(R.id.item_name);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -20,11 +20,16 @@ import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public abstract class DBAccess extends AsyncTask {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DBAccess.class);
|
||||
|
||||
private final String mTask;
|
||||
private final Context mContext;
|
||||
private Exception mError;
|
||||
@ -45,6 +50,7 @@ public abstract class DBAccess extends AsyncTask {
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
doInBackground(db);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error during DBAccess for {}", mTask, e);
|
||||
mError = e;
|
||||
}
|
||||
return null;
|
||||
|
@ -0,0 +1,15 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.deviceevents;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusicPlaylist;
|
||||
|
||||
public class GBDeviceMusicData extends GBDeviceEvent {
|
||||
public int type = 0; // 1 - sync start, 2 - music list, 10 - end sync
|
||||
public List<GBDeviceMusic> list = null;
|
||||
public List<GBDeviceMusicPlaylist> playlists = null;
|
||||
public String deviceInfo = null;
|
||||
public int maxMusicCount = 0;
|
||||
public int maxPlaylistCount = 0;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.deviceevents;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GBDeviceMusicUpdate extends GBDeviceEvent {
|
||||
public boolean success = false;
|
||||
public int operation = -1;
|
||||
public int playlistIndex = -1;
|
||||
public String playlistName;
|
||||
public ArrayList<Integer> musicIds = null;
|
||||
}
|
@ -95,7 +95,7 @@ public interface EventHandler {
|
||||
|
||||
void onAppConfiguration(UUID appUuid, String config, Integer id);
|
||||
|
||||
void onAppReorder(UUID uuids[]);
|
||||
void onAppReorder(UUID[] uuids);
|
||||
|
||||
void onFetchRecordedData(int dataTypes);
|
||||
|
||||
@ -154,4 +154,9 @@ public interface EventHandler {
|
||||
void onSleepAsAndroidAction(String action, Bundle extras);
|
||||
|
||||
void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename);
|
||||
|
||||
|
||||
void onMusicListReq();
|
||||
|
||||
void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds);
|
||||
}
|
||||
|
@ -55,7 +55,8 @@ public class BandWPSeriesDeviceCoordinator extends AbstractBLEDeviceCoordinator
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[] {
|
||||
R.xml.devicesettings_active_noise_cancelling_toggle,
|
||||
R.xml.devicesettings_bandw_pseries
|
||||
R.xml.devicesettings_bandw_pseries,
|
||||
R.xml.devicesettings_wear_sensor_toggle
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
@ -36,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps.GarminAgpsStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
@ -142,7 +144,10 @@ public class GarminSettingsCustomizer implements DeviceSpecificSettingsCustomize
|
||||
prefUpdateTime.setTitle(R.string.pref_agps_update_time);
|
||||
final long ts = prefs.getLong(GarminPreferences.agpsUpdateTime(url), 0L);
|
||||
if (ts > 0) {
|
||||
prefUpdateTime.setSummary(SDF.format(new Date(ts)));
|
||||
prefUpdateTime.setSummary(String.format("%s (%s)",
|
||||
SDF.format(new Date(ts)),
|
||||
DateTimeUtils.formatDurationHoursMinutes(System.currentTimeMillis() - ts, TimeUnit.MILLISECONDS)
|
||||
));
|
||||
} else {
|
||||
prefUpdateTime.setSummary(handler.getContext().getString(R.string.unknown));
|
||||
}
|
||||
|
@ -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 GarminForerunner55Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^Forerunner 55$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_forerunner_55;
|
||||
}
|
||||
}
|
@ -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 GarminForerunner620Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^Forerunner 620$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_forerunner_620;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminInstinct2Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^Instinct 2$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_instinct_2;
|
||||
}
|
||||
}
|
@ -305,6 +305,11 @@ public class HuaweiCoordinator {
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_contacts);
|
||||
}
|
||||
|
||||
//Music
|
||||
if (supportsMusicUploading() && getMusicInfoParams() != null && device.isConnected()) {
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_musicmanagement);
|
||||
}
|
||||
|
||||
// Time
|
||||
if (supportsDateFormat()) {
|
||||
final List<Integer> dateTime = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DATE_TIME);
|
||||
@ -448,6 +453,14 @@ public class HuaweiCoordinator {
|
||||
return supportsHeartRate() || getForceOption(gbDevice, PREF_FORCE_ENABLE_HEARTRATE_SUPPORT);
|
||||
}
|
||||
|
||||
public boolean supportsHeartRateZones() {
|
||||
return supportsCommandForService(0x07, 0x13);
|
||||
}
|
||||
|
||||
public boolean supportsExtendedHeartRateZones() {
|
||||
return supportsCommandForService(0x07, 0x21);
|
||||
}
|
||||
|
||||
public boolean supportsFitnessRestHeartRate() {
|
||||
return supportsCommandForService(0x07, 0x23);
|
||||
}
|
||||
@ -602,8 +615,6 @@ public class HuaweiCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean supportsCalendar() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(171) || supportsExpandCapability(184);
|
||||
@ -628,6 +639,12 @@ public class HuaweiCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsMoreMusic() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(122);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public boolean supportsPromptPushMessage () {
|
||||
// do not ask for capabilities under specific condition
|
||||
|
@ -7,16 +7,16 @@ import java.util.List;
|
||||
public class HuaweiMusicUtils {
|
||||
|
||||
public static class PageStruct {
|
||||
public short startIndex = 0;
|
||||
public short endIndex = 0;
|
||||
public short startFrame = 0;
|
||||
public short endFrame = 0;
|
||||
public short count = 0;
|
||||
public byte[] hashCode = null;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuffer sb = new StringBuffer("PageStruct{");
|
||||
sb.append("startIndex=").append(startIndex);
|
||||
sb.append(", endIndex=").append(endIndex);
|
||||
sb.append("startFrame=").append(startFrame);
|
||||
sb.append(", endFrame=").append(endFrame);
|
||||
sb.append(", count=").append(count);
|
||||
sb.append(", hashCode=");
|
||||
if (hashCode == null) sb.append("null");
|
||||
@ -68,7 +68,6 @@ public class HuaweiMusicUtils {
|
||||
public int currentMusicCount = 0; // TODO: not sure
|
||||
public int unknown = 0; // TODO: not sure
|
||||
public List<FormatRestrictions> formatsRestrictions = null;
|
||||
public List<PageStruct> pageStruct = null;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
@ -80,7 +79,6 @@ public class HuaweiMusicUtils {
|
||||
sb.append(", currentMusicCount=").append(currentMusicCount);
|
||||
sb.append(", unknown=").append(unknown);
|
||||
sb.append(", formatsRestrictions=").append(formatsRestrictions);
|
||||
sb.append(", pageStruct=").append(pageStruct);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
|
@ -598,6 +598,14 @@ public class HuaweiPacket {
|
||||
return new MusicControl.Control.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicInfoParams.id:
|
||||
return new MusicControl.MusicInfoParams.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicList.id:
|
||||
return new MusicControl.MusicList.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicPlaylists.id:
|
||||
return new MusicControl.MusicPlaylists.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicPlaylistMusics.id:
|
||||
return new MusicControl.MusicPlaylistMusics.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicOperation.id:
|
||||
return new MusicControl.MusicOperation.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.UploadMusicFileInfo.id:
|
||||
return new MusicControl.UploadMusicFileInfo.UploadMusicFileInfoRequest(paramsProvider).fromPacket(this);
|
||||
case MusicControl.ExtendedMusicInfoParams.id:
|
||||
|
@ -20,6 +20,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiReportThreshold;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
@ -597,6 +598,71 @@ public class FitnessData {
|
||||
}
|
||||
}
|
||||
|
||||
public static class HeartRateZoneConfigPacket {
|
||||
// It can use two IDs with basically the same format.
|
||||
public static final byte id_simple = 0x13;
|
||||
public static final byte id_extended = 0x21;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
private Request(
|
||||
ParamsProvider paramsProvider,
|
||||
byte id,
|
||||
HeartRateZonesConfig heartRateZonesConfig
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
|
||||
HuaweiTLV subTlv = new HuaweiTLV().
|
||||
put(0x08, heartRateZonesConfig.getWarningEnable());
|
||||
|
||||
if (
|
||||
heartRateZonesConfig.hasValidMHRData() &&
|
||||
heartRateZonesConfig.getWarningHRLimit() > 0 &&
|
||||
heartRateZonesConfig.getMaxHRThreshold() > 0
|
||||
) {
|
||||
subTlv
|
||||
.put(0x09, (byte) heartRateZonesConfig.getWarningHRLimit())
|
||||
.put(0x02, (byte) heartRateZonesConfig.getMHRWarmUp())
|
||||
.put(0x03, (byte) heartRateZonesConfig.getMHRFatBurning())
|
||||
.put(0x04, (byte) heartRateZonesConfig.getMHRAerobic())
|
||||
.put(0x05, (byte) heartRateZonesConfig.getMHRAnaerobic())
|
||||
.put(0x06, (byte) heartRateZonesConfig.getMHRExtreme())
|
||||
.put(0x07, (byte) heartRateZonesConfig.getMaxHRThreshold())
|
||||
.put(0x0b, (byte) heartRateZonesConfig.getMaxHRThreshold());
|
||||
}
|
||||
|
||||
if (id == id_extended && heartRateZonesConfig.hasValidHRRData()) {
|
||||
subTlv
|
||||
.put(0x0d, (byte) heartRateZonesConfig.getHRRBasicAerobic())
|
||||
.put(0x0e, (byte) heartRateZonesConfig.getHRRAdvancedAerobic())
|
||||
.put(0x0f, (byte) heartRateZonesConfig.getHRRLactate())
|
||||
.put(0x10, (byte) heartRateZonesConfig.getHRRBasicAnaerobic())
|
||||
.put(0x11, (byte) heartRateZonesConfig.getHRRAdvancedAnaerobic());
|
||||
}
|
||||
|
||||
if (id == id_extended && heartRateZonesConfig.getRestHeartRate() > 0) {
|
||||
subTlv
|
||||
.put(0x0a, (byte) heartRateZonesConfig.getCalculateMethod())
|
||||
.put(0x0c, (byte) heartRateZonesConfig.getRestHeartRate());
|
||||
}
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x81, subTlv);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
|
||||
public static Request requestSimple(ParamsProvider paramsProvider, HeartRateZonesConfig heartRateZonesConfig) {
|
||||
return new Request(paramsProvider, id_simple, heartRateZonesConfig);
|
||||
}
|
||||
|
||||
public static Request requestExtended(ParamsProvider paramsProvider, HeartRateZonesConfig heartRateZonesConfig) {
|
||||
return new Request(paramsProvider, id_extended, heartRateZonesConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TruSleep {
|
||||
public static final byte id = 0x16;
|
||||
|
||||
|
@ -18,12 +18,14 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiMusicUtils.parseFormatBits;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiMusicUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
|
||||
public class MusicControl {
|
||||
public static final byte id = 0x25;
|
||||
@ -251,16 +253,16 @@ public class MusicControl {
|
||||
public static class Response extends HuaweiPacket {
|
||||
public HuaweiMusicUtils.MusicCapabilities params = new HuaweiMusicUtils.MusicCapabilities();
|
||||
|
||||
public int frameCount = 0;
|
||||
public List<HuaweiMusicUtils.PageStruct> pageStruct = null;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
|
||||
//TODO: unknown TLV
|
||||
// if (this.tlv.contains(0x01))
|
||||
// LOG.info("Unknown: " + this.tlv.getShort(0x01));
|
||||
this.frameCount = this.tlv.getAsInteger(0x01);
|
||||
|
||||
if (this.tlv.contains(0x02))
|
||||
params.availableSpace = this.tlv.getAsInteger(0x02);
|
||||
@ -274,25 +276,214 @@ public class MusicControl {
|
||||
params.currentMusicCount = this.tlv.getAsInteger(0x05);
|
||||
|
||||
if (this.tlv.contains(0x86)) {
|
||||
params.pageStruct = new ArrayList<>();
|
||||
this.pageStruct = new ArrayList<>();
|
||||
List<HuaweiTLV> subTlvs = this.tlv.getObject(0x86).getObjects(0x87);
|
||||
for (HuaweiTLV subTlv : subTlvs) {
|
||||
HuaweiMusicUtils.PageStruct pageStruct = new HuaweiMusicUtils.PageStruct();
|
||||
if (subTlv.contains(0x08))
|
||||
pageStruct.startIndex = subTlv.getShort(0x08);
|
||||
pageStruct.startFrame = subTlv.getShort(0x08);
|
||||
if (subTlv.contains(0x09))
|
||||
pageStruct.endIndex = subTlv.getShort(0x09);
|
||||
pageStruct.endFrame = subTlv.getShort(0x09);
|
||||
if (subTlv.contains(0x0a))
|
||||
pageStruct.count = subTlv.getShort(0x0a);
|
||||
if (subTlv.contains(0x0b))
|
||||
pageStruct.hashCode = subTlv.getBytes(0x0b);
|
||||
params.pageStruct.add(pageStruct);
|
||||
this.pageStruct.add(pageStruct);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MusicList {
|
||||
public static final byte id = 0x05;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, short startFrame, short endIndex) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, startFrame)
|
||||
.put(0x04, endIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
|
||||
public short startFrame = 0;
|
||||
public short endIndex = 0;
|
||||
|
||||
public List<GBDeviceMusic> musicList;
|
||||
public Response (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws HuaweiPacket.ParseException {
|
||||
if(tlv.contains(0x1))
|
||||
startFrame = tlv.getShort(0x1);
|
||||
if(tlv.contains(0x4))
|
||||
endIndex = tlv.getShort(0x4);
|
||||
musicList = new ArrayList<>();
|
||||
if(this.tlv.contains(0x82)) {
|
||||
for (HuaweiTLV subTlv : this.tlv.getObject(0x82).getObjects(0x83)) {
|
||||
int index = subTlv.getAsInteger(0x4);
|
||||
String title = subTlv.getString(0x5);
|
||||
String artist = subTlv.getString(0x6);
|
||||
String fileName = subTlv.getString(0x7);
|
||||
musicList.add(new GBDeviceMusic(index, title, artist, fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MusicPlaylists {
|
||||
public static final byte id = 0x06;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
|
||||
public static class PlaylistData {
|
||||
public int id;
|
||||
public String name;
|
||||
public int frameCount;
|
||||
}
|
||||
|
||||
public List<PlaylistData> playlists = new ArrayList<>();
|
||||
|
||||
public Response (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws HuaweiPacket.ParseException {
|
||||
if(this.tlv.contains(0x81)) {
|
||||
for (HuaweiTLV subTlv : this.tlv.getObject(0x81).getObjects(0x82)) {
|
||||
PlaylistData data = new PlaylistData();
|
||||
data.id = subTlv.getAsInteger(0x3);
|
||||
data.name = subTlv.getString(0x4);
|
||||
data.frameCount = subTlv.getAsInteger(0x5);
|
||||
playlists.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MusicPlaylistMusics {
|
||||
public static final byte id = 0x07;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, short playlist, short index) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, playlist)
|
||||
.put(0x02, index);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
|
||||
public int id = -1;
|
||||
public int index = -1;
|
||||
public ArrayList<Integer> musicIds = null;
|
||||
|
||||
public Response (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws HuaweiPacket.ParseException {
|
||||
if(this.tlv.contains(0x1))
|
||||
id = tlv.getAsInteger(0x1);
|
||||
if(this.tlv.contains(0x2))
|
||||
index = tlv.getAsInteger(0x2);
|
||||
|
||||
if(this.tlv.contains(0x3)) {
|
||||
musicIds = new ArrayList<>();
|
||||
ByteBuffer dt = ByteBuffer.wrap(this.tlv.getBytes(0x3));
|
||||
while (dt.hasRemaining())
|
||||
musicIds.add((int) dt.getShort());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MusicOperation {
|
||||
public static final byte id = 0x08;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, (byte)operation);
|
||||
|
||||
if(operation == 1 || operation == 2 || operation == 3 || operation == 4) {
|
||||
this.tlv.put(0x02, (short)playlistIndex);
|
||||
}
|
||||
if (operation == 0 || operation == 2) {
|
||||
this.tlv.put(0x03, playlistName);
|
||||
}
|
||||
|
||||
if (operation == 3 || operation == 4) {
|
||||
ByteBuffer ids = ByteBuffer.allocate(musicIds.size() * 2);
|
||||
for (Integer id : musicIds) {
|
||||
ids.putShort(id.shortValue());
|
||||
}
|
||||
this.tlv.put(0x04, ids.array());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
|
||||
public int operation = -1;
|
||||
public int playlistIndex = -1;
|
||||
public String playlistName;
|
||||
public ArrayList<Integer> musicIds = null;
|
||||
public int resultCode = -1;
|
||||
|
||||
public Response (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws HuaweiPacket.ParseException {
|
||||
if(this.tlv.contains(0x7f))
|
||||
resultCode = tlv.getInteger(0x7f);
|
||||
if(this.tlv.contains(0x1))
|
||||
operation = tlv.getByte(0x1);
|
||||
if(this.tlv.contains(0x2))
|
||||
playlistIndex = tlv.getAsInteger(0x2);
|
||||
if(this.tlv.contains(0x3))
|
||||
playlistName = tlv.getString(0x3);
|
||||
|
||||
if(this.tlv.contains(0x4)) {
|
||||
musicIds = new ArrayList<>();
|
||||
ByteBuffer dt = ByteBuffer.wrap(this.tlv.getBytes(0x4));
|
||||
while (dt.hasRemaining())
|
||||
musicIds.add((int) dt.getShort());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class ExtendedMusicInfoParams {
|
||||
public static final byte id = 0x0d;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,435 @@
|
||||
/* 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 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_DAGPT = (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";
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,275 @@
|
||||
/* 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.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.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 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_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.ACTIVITY)
|
||||
return ACTIVITY_NOT_MEASURED; // TODO: ?
|
||||
else
|
||||
throw new IllegalArgumentException("Invalid Gadgetbridge activity kind: " + activityKind);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Add empty dummy samples every 5 min to make sure the charts and stats aren't too malformed
|
||||
// This is necessary due to the Colmi rings just reporting steps/calories/distance aggregates per hour
|
||||
// for (int i=timestamp_from; i<=timestamp_to; i+=300) {
|
||||
// MoyoungActivitySample sample = sampleByTs.get(i);
|
||||
// if (sample == null) {
|
||||
// sample = new MoyoungActivitySample();
|
||||
// sample.setTimestamp(i);
|
||||
// sample.setProvider(this);
|
||||
// sample.setRawKind(ActivitySample.NOT_MEASURED);
|
||||
// sampleByTs.put(i, sample);
|
||||
// }
|
||||
// }
|
||||
|
||||
final List<MoyoungActivitySample> finalSamples = new ArrayList<>(sampleByTs.values());
|
||||
Collections.sort(finalSamples, (a, b) -> Integer.compare(a.getTimestamp(), b.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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/* 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.oppo;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue;
|
||||
|
||||
public class OppoEncoAirCoordinator extends OppoHeadphonesCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("OPPO Enco Air", Pattern.LITERAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_oppo_enco_air;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions() {
|
||||
return new LinkedHashMap<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>>() {{
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_2), Arrays.asList(
|
||||
TouchConfigValue.OFF,
|
||||
TouchConfigValue.PLAY_PAUSE,
|
||||
TouchConfigValue.PREVIOUS,
|
||||
TouchConfigValue.NEXT,
|
||||
TouchConfigValue.VOICE_ASSISTANT
|
||||
));
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_3), Arrays.asList(
|
||||
TouchConfigValue.OFF,
|
||||
TouchConfigValue.VOICE_ASSISTANT,
|
||||
TouchConfigValue.GAME_MODE
|
||||
));
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.HOLD), Arrays.asList(
|
||||
TouchConfigValue.OFF,
|
||||
TouchConfigValue.VOLUME_UP,
|
||||
TouchConfigValue.VOLUME_DOWN
|
||||
));
|
||||
|
||||
// Right side is the same
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_2), get(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_2)));
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_3), get(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_3)));
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.HOLD), get(Pair.create(TouchConfigSide.LEFT, TouchConfigType.HOLD)));
|
||||
}};
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/* 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.oppo;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.OppoHeadphonesSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue;
|
||||
|
||||
public abstract class OppoHeadphonesCoordinator extends AbstractBLClassicDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull final GBDevice gbDevice, @NonNull final Device device, @NonNull final DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Oppo";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return OppoHeadphonesSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_nothingear;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDisabledIconResource() {
|
||||
return R.drawable.ic_device_nothingear_disabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatteryCount() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatteryConfig[] getBatteryConfig(final GBDevice device) {
|
||||
final BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_nothing_ear_l, R.string.left_earbud);
|
||||
final BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_r, R.string.right_earbud);
|
||||
final BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_tws_case, R.string.battery_case);
|
||||
return new BatteryConfig[]{battery1, battery2, battery3};
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||
final DeviceSpecificSettings settings = new DeviceSpecificSettings();
|
||||
|
||||
settings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS);
|
||||
settings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_oppo_headphones_touch_options);
|
||||
|
||||
settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
|
||||
settings.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
|
||||
return new OppoHeadphonesSettingsCustomizer(getTouchOptions());
|
||||
}
|
||||
|
||||
protected abstract Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions();
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/* 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.oppo;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
|
||||
public class OppoHeadphonesPreferences {
|
||||
public static String getKey(final TouchConfigSide side, final TouchConfigType type) {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"oppo_touch__%s__%s",
|
||||
side.name().toLowerCase(Locale.ROOT),
|
||||
type.name().toLowerCase(Locale.ROOT)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/* 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.oppo;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
|
||||
private final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions;
|
||||
|
||||
public static final Creator<OppoHeadphonesSettingsCustomizer> CREATOR = new Creator<OppoHeadphonesSettingsCustomizer>() {
|
||||
@Override
|
||||
public OppoHeadphonesSettingsCustomizer createFromParcel(final Parcel in) {
|
||||
final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions = new LinkedHashMap<>();
|
||||
final int numOptions = in.readInt();
|
||||
for (int i = 0; i < numOptions; i++) {
|
||||
final TouchConfigSide touchConfigSide = TouchConfigSide.valueOf(in.readString());
|
||||
final TouchConfigType touchConfigType = TouchConfigType.valueOf(in.readString());
|
||||
final List<TouchConfigValue> values = new ArrayList<>();
|
||||
in.readList(values, TouchConfigValue.class.getClassLoader());
|
||||
touchOptions.put(Pair.create(touchConfigSide, touchConfigType), values);
|
||||
}
|
||||
return new OppoHeadphonesSettingsCustomizer(touchOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OppoHeadphonesSettingsCustomizer[] newArray(final int size) {
|
||||
return new OppoHeadphonesSettingsCustomizer[size];
|
||||
}
|
||||
};
|
||||
|
||||
public OppoHeadphonesSettingsCustomizer(final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions) {
|
||||
this.touchOptions = touchOptions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final String rootKey) {
|
||||
final Set<TouchConfigSide> knownSides = new HashSet<>();
|
||||
final Set<TouchConfigType> knownTypes = new HashSet<>();
|
||||
|
||||
for (final Map.Entry<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) {
|
||||
final TouchConfigSide side = e.getKey().first;
|
||||
final TouchConfigType type = e.getKey().second;
|
||||
final Set<TouchConfigValue> possibleValues = new HashSet<>(e.getValue());
|
||||
|
||||
knownSides.add(side);
|
||||
knownTypes.add(type);
|
||||
|
||||
final String key = OppoHeadphonesPreferences.getKey(side, type);
|
||||
final ListPreference pref = handler.findPreference(key);
|
||||
if (pref == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final CharSequence[] originalEntries = pref.getEntries();
|
||||
final CharSequence[] originalValues = pref.getEntryValues();
|
||||
final CharSequence[] entries = new CharSequence[possibleValues.size()];
|
||||
final CharSequence[] values = new CharSequence[possibleValues.size()];
|
||||
int j = 0;
|
||||
for (int i = 0; i < originalValues.length; i++) {
|
||||
if (possibleValues.contains(TouchConfigValue.valueOf(originalValues[i].toString().toUpperCase(Locale.ROOT)))) {
|
||||
entries[j] = originalEntries[i];
|
||||
values[j] = originalValues[i];
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
pref.setEntries(entries);
|
||||
pref.setEntryValues(values);
|
||||
|
||||
handler.addPreferenceHandlerFor(key);
|
||||
}
|
||||
|
||||
for (final TouchConfigSide side : TouchConfigSide.values()) {
|
||||
if (!knownSides.contains(side)) {
|
||||
// Side not configurable, hide it completely
|
||||
final Preference header = handler.findPreference("oppo_touch_header_" + side.name().toLowerCase(Locale.ROOT));
|
||||
if (header != null) {
|
||||
header.setVisible(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (final TouchConfigType type : TouchConfigType.values()) {
|
||||
if (!knownTypes.contains(type)) {
|
||||
final String key = OppoHeadphonesPreferences.getKey(side, type);
|
||||
final Preference pref = handler.findPreference(key);
|
||||
if (pref != null) {
|
||||
pref.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getPreferenceKeysWithSummary() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeInt(touchOptions.size());
|
||||
for (final Map.Entry<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) {
|
||||
dest.writeString(e.getKey().first.name());
|
||||
dest.writeString(e.getKey().second.name());
|
||||
dest.writeList(e.getValue());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/* 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.realme;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoHeadphonesCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue;
|
||||
|
||||
public class RealmeBudsT110Coordinator extends OppoHeadphonesCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("realme Buds T110", Pattern.LITERAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Realme";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_realme_buds_t110;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions() {
|
||||
return new LinkedHashMap<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>>() {{
|
||||
final List<TouchConfigValue> options = Arrays.asList(
|
||||
TouchConfigValue.OFF,
|
||||
TouchConfigValue.PLAY_PAUSE,
|
||||
TouchConfigValue.PREVIOUS,
|
||||
TouchConfigValue.NEXT,
|
||||
TouchConfigValue.VOLUME_UP,
|
||||
TouchConfigValue.VOLUME_DOWN,
|
||||
TouchConfigValue.VOICE_ASSISTANT_REALME
|
||||
);
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_2), options);
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_3), options);
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.HOLD), options);
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_2), options);
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_3), options);
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.HOLD), options);
|
||||
put(Pair.create(TouchConfigSide.BOTH, TouchConfigType.HOLD), Arrays.asList(
|
||||
TouchConfigValue.OFF,
|
||||
TouchConfigValue.GAME_MODE
|
||||
));
|
||||
}};
|
||||
}
|
||||
}
|
@ -38,7 +38,9 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
|
||||
@ -502,26 +504,26 @@ public class TestDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(final GBDevice device) {
|
||||
final List<Integer> settings = new ArrayList<>();
|
||||
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
|
||||
|
||||
settings.add(R.xml.devicesettings_header_apps);
|
||||
settings.add(R.xml.devicesettings_loyalty_cards);
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_loyalty_cards);
|
||||
|
||||
if (getWorldClocksSlotCount() > 0) {
|
||||
settings.add(R.xml.devicesettings_header_time);
|
||||
settings.add(R.xml.devicesettings_world_clocks);
|
||||
final List<Integer> dateTime = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DATE_TIME);
|
||||
dateTime.add(R.xml.devicesettings_world_clocks);
|
||||
}
|
||||
|
||||
if (getContactsSlotCount(device) > 0) {
|
||||
settings.add(R.xml.devicesettings_header_other);
|
||||
settings.add(R.xml.devicesettings_contacts);
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_contacts);
|
||||
}
|
||||
|
||||
settings.add(R.xml.devicesettings_header_developer);
|
||||
settings.add(R.xml.devicesettings_test_features);
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_test_features);
|
||||
|
||||
return ArrayUtils.toPrimitive(settings.toArray(new Integer[0]));
|
||||
final List<Integer> developer = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER);
|
||||
developer.add(R.xml.devicesettings_developer_add_test_activities);
|
||||
|
||||
return deviceSpecificSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -17,15 +17,30 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.test;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.MultiSelectListPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class TestDeviceSpecificSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
|
||||
@ -39,20 +54,55 @@ public class TestDeviceSpecificSettingsCustomizer implements DeviceSpecificSetti
|
||||
@Override
|
||||
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final String rootKey) {
|
||||
final Preference pref = handler.findPreference(TestDeviceConst.PREF_TEST_FEATURES);
|
||||
if (pref == null) {
|
||||
return;
|
||||
if (pref != null) {
|
||||
// Populate the preference directly from the enum
|
||||
final CharSequence[] entries = new CharSequence[TestFeature.values().length];
|
||||
final CharSequence[] values = new CharSequence[TestFeature.values().length];
|
||||
for (int i = 0; i < TestFeature.values().length; i++) {
|
||||
entries[i] = TestFeature.values()[i].name();
|
||||
values[i] = TestFeature.values()[i].name();
|
||||
}
|
||||
if (pref instanceof MultiSelectListPreference) {
|
||||
((MultiSelectListPreference) pref).setEntries(entries);
|
||||
((MultiSelectListPreference) pref).setEntryValues(values);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the preference directly from the enum
|
||||
final CharSequence[] entries = new CharSequence[TestFeature.values().length];
|
||||
final CharSequence[] values = new CharSequence[TestFeature.values().length];
|
||||
for (int i = 0; i < TestFeature.values().length; i++) {
|
||||
entries[i] = TestFeature.values()[i].name();
|
||||
values[i] = TestFeature.values()[i].name();
|
||||
}
|
||||
if (pref instanceof MultiSelectListPreference) {
|
||||
((MultiSelectListPreference) pref).setEntries(entries);
|
||||
((MultiSelectListPreference) pref).setEntryValues(values);
|
||||
final Preference addTestActivities = handler.findPreference("pref_developer_add_test_activities");
|
||||
if (addTestActivities != null) {
|
||||
addTestActivities.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull final Preference preference) {
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = dbHandler.getDaoSession();
|
||||
final Device device = DBHelper.getDevice(handler.getDevice(), session);
|
||||
final User user = DBHelper.getUser(session);
|
||||
|
||||
//final QueryBuilder<?> qb = session.getBaseActivitySummaryDao().queryBuilder();
|
||||
//qb.where(BaseActivitySummaryDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
final List<BaseActivitySummary> summaries = new ArrayList<>();
|
||||
|
||||
for (final ActivityKind activityKind : ActivityKind.values()) {
|
||||
final BaseActivitySummary summary = new BaseActivitySummary();
|
||||
summary.setStartTime(new Date(System.currentTimeMillis() - new Random().nextInt(31 * 24 * 60 * 60) * 1000L));
|
||||
summary.setEndTime(new Date(summary.getStartTime().getTime() + new Random().nextInt(60 * 60 * 2) * 1000L));
|
||||
summary.setDevice(device);
|
||||
summary.setUser(user);
|
||||
summary.setActivityKind(activityKind.getCode());
|
||||
// TODO data
|
||||
summaries.add(summary);
|
||||
}
|
||||
|
||||
session.getBaseActivitySummaryDao().insertOrReplaceInTx(summaries);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(handler.getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() +
|
||||
"}";
|
||||
}
|
||||
}
|
@ -455,10 +455,14 @@ public class NotificationListener extends NotificationListenerService {
|
||||
mPackageLookup.add(notificationSpec.getId(), sbn.getPackageName()); // for MUTE
|
||||
|
||||
notificationBurstPrevention.put(source, curTime);
|
||||
if (0 != notification.when) {
|
||||
notificationOldRepeatPrevention.put(source, notification.when);
|
||||
if (notification.when == 0) {
|
||||
LOG.info("This app might show old/duplicate notifications. notification.when is 0 for {}", source);
|
||||
} else if ((notification.when - System.currentTimeMillis()) > 30_000L) {
|
||||
// #4327 - Some apps such as outlook send reminder notifications in the future
|
||||
// If we add them to the oldRepeatPrevention, they never show up again
|
||||
LOG.info("This app might show old/duplicate notifications. notification.when is in the future for {}", source);
|
||||
} else {
|
||||
LOG.info("This app might show old/duplicate notifications. notification.when is 0 for " + source);
|
||||
notificationOldRepeatPrevention.put(source, notification.when);
|
||||
}
|
||||
notificationsActive.add(notificationSpec.getId());
|
||||
// NOTE for future developers: this call goes to implementations of DeviceService.onNotification(NotificationSpec), like in GBDeviceService
|
||||
@ -813,10 +817,11 @@ public class NotificationListener extends NotificationListenerService {
|
||||
|
||||
private void logNotification(StatusBarNotification sbn, boolean posted) {
|
||||
LOG.debug(
|
||||
"Notification {} {}: packageName={}, priority={}, category={}",
|
||||
"Notification {} {}: packageName={}, when={}, priority={}, category={}",
|
||||
sbn.getId(),
|
||||
posted ? "posted" : "removed",
|
||||
sbn.getPackageName(),
|
||||
sbn.getNotification().when,
|
||||
sbn.getNotification().priority,
|
||||
sbn.getNotification().category
|
||||
);
|
||||
|
@ -400,15 +400,12 @@ public class GBDevice implements Parcelable {
|
||||
}
|
||||
|
||||
private void unsetDynamicState() {
|
||||
|
||||
setBatteryLevel(BATTERY_UNKNOWN, 0);
|
||||
setBatteryLevel(BATTERY_UNKNOWN, 1);
|
||||
setBatteryLevel(BATTERY_UNKNOWN, 2);
|
||||
setBatteryState(UNKNOWN, 0);
|
||||
setBatteryState(UNKNOWN, 1);
|
||||
setBatteryState(UNKNOWN, 2);
|
||||
setFirmwareVersion(null);
|
||||
setFirmwareVersion2(null);
|
||||
setRssi(RSSI_UNKNOWN);
|
||||
resetExtraInfos();
|
||||
if (mBusyTask != null) {
|
||||
|
@ -0,0 +1,33 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.impl;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class GBDeviceMusic implements Serializable {
|
||||
private final int id;
|
||||
private final String title;
|
||||
private final String artist;
|
||||
private final String fileName;
|
||||
|
||||
public GBDeviceMusic(int id, String title, String artist, String fileName) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.artist = artist;
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getArtist() {
|
||||
return artist;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.impl;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GBDeviceMusicPlaylist implements Serializable {
|
||||
private final int id;
|
||||
private String name;
|
||||
private ArrayList<Integer> musicIds;
|
||||
|
||||
public GBDeviceMusicPlaylist(int id, String name, ArrayList<Integer> musicIds) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.musicIds = musicIds;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public ArrayList<Integer> getMusicIds() {
|
||||
return musicIds;
|
||||
}
|
||||
|
||||
public void setMusicIds(ArrayList<Integer> musicIds) {
|
||||
this.musicIds = musicIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
@ -568,4 +568,20 @@ public class GBDeviceService implements DeviceService {
|
||||
intent.putExtra(EXTRA_CAMERA_FILENAME, filename);
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicListReq() {
|
||||
Intent intent = createIntent().setAction(ACTION_REQUEST_MUSIC_LIST);
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
Intent intent = createIntent().setAction(ACTION_REQUEST_MUSIC_OPERATION);
|
||||
intent.putExtra("operation", operation);
|
||||
intent.putExtra("playlistIndex", playlistIndex);
|
||||
intent.putExtra("playlistName", playlistName);
|
||||
intent.putExtra("musicIds", musicIds);
|
||||
invokeService(intent);
|
||||
}
|
||||
}
|
||||
|
@ -67,8 +67,8 @@ public enum ActivityKind {
|
||||
HANDCYCLING_INDOOR(0x04000005, R.string.activity_type_handcycling_indoor),
|
||||
TRANSITION(0x04000006, R.string.activity_type_transition),
|
||||
FITNESS_EQUIPMENT(0x04000007, R.string.activity_type_fitness_equipment),
|
||||
STAIR_STEPPER(0x04000008, R.string.activity_type_stair_stepper),
|
||||
PILATES(0x04000009, R.string.activity_type_pilates),
|
||||
STAIR_STEPPER(0x04000008, R.string.activity_type_stair_stepper, R.drawable.ic_activity_stair_stepper),
|
||||
PILATES(0x04000009, R.string.activity_type_pilates, R.drawable.ic_activity_pilates),
|
||||
POOL_SWIM(0x0400000a, R.string.activity_type_pool_swimming, R.drawable.ic_activity_swimming),
|
||||
TENNIS(0x0400000b, R.string.activity_type_tennis),
|
||||
PLATFORM_TENNIS(0x0400000c, R.string.activity_type_platform_tennis),
|
||||
@ -94,23 +94,23 @@ public enum ActivityKind {
|
||||
HUNTING(0x04000023, R.string.activity_type_hunting),
|
||||
FISHING(0x04000024, R.string.activity_type_fishing),
|
||||
INLINE_SKATING(0x04000025, R.string.activity_type_inline_skating),
|
||||
ROCK_CLIMBING(0x04000026, R.string.activity_type_rock_climbing),
|
||||
ROCK_CLIMBING(0x04000026, R.string.activity_type_rock_climbing, R.drawable.ic_activity_rock_climbing),
|
||||
CLIMB_INDOOR(0x04000027, R.string.activity_type_climb_indoor),
|
||||
BOULDERING(0x04000028, R.string.activity_type_bouldering),
|
||||
SAIL_RACE(0x0400002a, R.string.activity_type_sail_race, R.drawable.ic_activity_sailing),
|
||||
SAIL_EXPEDITION(0x0400002b, R.string.activity_type_sail_expedition, R.drawable.ic_activity_sailing),
|
||||
ICE_SKATING(0x0400002c, R.string.activity_type_ice_skating),
|
||||
ICE_SKATING(0x0400002c, R.string.activity_type_ice_skating, R.drawable.ic_activity_ice_skating),
|
||||
SKY_DIVING(0x0400002d, R.string.activity_type_sky_diving),
|
||||
SNOWSHOE(0x0400002e, R.string.activity_type_snowshoe),
|
||||
SNOWMOBILING(0x0400002f, R.string.activity_type_snowmobiling),
|
||||
STAND_UP_PADDLEBOARDING(0x04000030, R.string.activity_type_stand_up_paddleboarding),
|
||||
SURFING(0x04000031, R.string.activity_type_surfing),
|
||||
WAKEBOARDING(0x04000032, R.string.activity_type_wakeboarding),
|
||||
WATER_SKIING(0x04000033, R.string.activity_type_water_skiing),
|
||||
STAND_UP_PADDLEBOARDING(0x04000030, R.string.activity_type_stand_up_paddleboarding, R.drawable.ic_activity_sup),
|
||||
SURFING(0x04000031, R.string.activity_type_surfing, R.drawable.ic_activity_surfing),
|
||||
WAKEBOARDING(0x04000032, R.string.activity_type_wakeboarding, R.drawable.ic_activity_wakeboarding),
|
||||
WATER_SKIING(0x04000033, R.string.activity_type_water_skiing, R.drawable.ic_activity_waterskiing),
|
||||
KAYAKING(0x04000034, R.string.activity_type_kayaking, R.drawable.ic_activity_rowing),
|
||||
RAFTING(0x04000035, R.string.activity_type_rafting, R.drawable.ic_activity_rowing),
|
||||
WINDSURFING(0x04000036, R.string.activity_type_windsurfing),
|
||||
KITESURFING(0x04000037, R.string.activity_type_kitesurfing),
|
||||
WINDSURFING(0x04000036, R.string.activity_type_windsurfing, R.drawable.ic_activity_windsurfing),
|
||||
KITESURFING(0x04000037, R.string.activity_type_kitesurfing, R.drawable.ic_activity_kitesurfing),
|
||||
TACTICAL(0x04000038, R.string.activity_type_tactical),
|
||||
JUMPMASTER(0x04000039, R.string.activity_type_jumpmaster),
|
||||
BOXING(0x0400003a, R.string.activity_type_boxing),
|
||||
@ -144,7 +144,7 @@ public enum ActivityKind {
|
||||
HOCKEY(0x04000056, R.string.activity_type_hockey),
|
||||
LACROSSE(0x04000057, R.string.activity_type_lacrosse),
|
||||
VOLLEYBALL(0x04000058, R.string.activity_type_volleyball),
|
||||
WATER_TUBING(0x04000059, R.string.activity_type_water_tubing),
|
||||
WATER_TUBING(0x04000059, R.string.activity_type_water_tubing, R.drawable.ic_activity_watertubing),
|
||||
WAKESURFING(0x0400005a, R.string.activity_type_wakesurfing),
|
||||
MIXED_MARTIAL_ARTS(0x0400005b, R.string.activity_type_mixed_martial_arts), // aka MMA
|
||||
DANCE(0x0400005c, R.string.activity_type_dance),
|
||||
@ -194,20 +194,20 @@ public enum ActivityKind {
|
||||
ROLLER_SKATING(0x04000087, R.string.activity_type_roller_skating),
|
||||
MARTIAL_ARTS(0x04000088, R.string.activity_type_martial_arts),
|
||||
TAI_CHI(0x04000089, R.string.activity_type_tai_chi),
|
||||
HULA_HOOPING(0x0400008a, R.string.activity_type_hula_hooping),
|
||||
HULA_HOOPING(0x0400008a, R.string.activity_type_hula_hooping, R.drawable.ic_activity_hula_hoop),
|
||||
DISC_SPORTS(0x0400008b, R.string.activity_type_disc_sports),
|
||||
DARTS(0x0400008c, R.string.activity_type_darts),
|
||||
ARCHERY(0x0400008d, R.string.activity_type_archery),
|
||||
ARCHERY(0x0400008d, R.string.activity_type_archery, R.drawable.ic_activity_archery),
|
||||
HORSE_RIDING(0x0400008e, R.string.activity_type_horse_riding),
|
||||
KITE_FLYING(0x0400008f, R.string.activity_type_kite_flying),
|
||||
SWING(0x04000090, R.string.activity_type_swing),
|
||||
STAIRS(0x04000091, R.string.activity_type_stairs),
|
||||
STAIRS(0x04000091, R.string.activity_type_stairs, R.drawable.ic_activity_stairs),
|
||||
MIND_AND_BODY(0x04000092, R.string.activity_type_mind_and_body),
|
||||
WRESTLING(0x04000093, R.string.activity_type_wrestling),
|
||||
KABADDI(0x04000094, R.string.activity_type_kabaddi),
|
||||
KARTING(0x04000095, R.string.activity_type_karting),
|
||||
BILLIARDS(0x04000096, R.string.activity_type_billiards),
|
||||
BOWLING(0x04000097, R.string.activity_type_bowling),
|
||||
BOWLING(0x04000097, R.string.activity_type_bowling, R.drawable.ic_activity_bowling),
|
||||
SHUTTLECOCK(0x04000098, R.string.activity_type_shuttlecock),
|
||||
HANDBALL(0x04000099, R.string.activity_type_handball),
|
||||
DODGEBALL(0x0400009a, R.string.activity_type_dodgeball),
|
||||
@ -222,14 +222,14 @@ public enum ActivityKind {
|
||||
JET_SKIING(0x040000a3, R.string.activity_type_jet_skiing),
|
||||
SKATING(0x040000a4, R.string.activity_type_skating),
|
||||
ICE_HOCKEY(0x040000a5, R.string.activity_type_ice_hockey),
|
||||
CURLING(0x040000a6, R.string.activity_type_curling),
|
||||
CURLING(0x040000a6, R.string.activity_type_curling, R.drawable.ic_activity_curling),
|
||||
CROSS_COUNTRY_SKIING(0x040000a8, R.string.activity_type_cross_country_skiing),
|
||||
SNOW_SPORTS(0x040000a9, R.string.activity_type_snow_sports),
|
||||
LUGE(0x040000ab, R.string.activity_type_luge),
|
||||
SKATEBOARDING(0x040000ac, R.string.activity_type_skateboarding),
|
||||
PARACHUTING(0x040000ae, R.string.activity_type_parachuting),
|
||||
PARKOUR(0x040000af, R.string.activity_type_parkour),
|
||||
INDOOR_RUNNING(0x040000b0, R.string.activity_type_indoor_running),
|
||||
INDOOR_RUNNING(0x040000b0, R.string.activity_type_indoor_running, R.drawable.ic_activity_indoor_running),
|
||||
OUTDOOR_RUNNING(0x040000b1, R.string.activity_type_outdoor_running, R.drawable.ic_activity_running),
|
||||
OUTDOOR_WALKING(0x040000b2, R.string.activity_type_outdoor_walking, R.drawable.ic_activity_hiking),
|
||||
OUTDOOR_CYCLING(0x040000b3, R.string.activity_type_outdoor_cycling, R.drawable.ic_activity_biking),
|
||||
@ -251,13 +251,13 @@ public enum ActivityKind {
|
||||
FINSWIMMING(0x040000c3, R.string.activity_type_finswimming),
|
||||
FLOWRIDING(0x040000c4, R.string.activity_type_flowriding),
|
||||
FOLK_DANCE(0x040000c5, R.string.activity_type_folk_dance),
|
||||
FRISBEE(0x040000c6, R.string.activity_type_frisbee),
|
||||
FRISBEE(0x040000c6, R.string.activity_type_frisbee, R.drawable.ic_activity_frisbee),
|
||||
FUTSAL(0x040000c7, R.string.activity_type_futsal),
|
||||
HACKY_SACK(0x040000c8, R.string.activity_type_hacky_sack),
|
||||
HIP_HOP(0x040000c9, R.string.activity_type_hip_hop),
|
||||
HULA_HOOP(0x040000ca, R.string.activity_type_hula_hoop),
|
||||
HULA_HOOP(0x040000ca, R.string.activity_type_hula_hoop, R.drawable.ic_activity_hula_hoop),
|
||||
INDOOR_FITNESS(0x040000cb, R.string.activity_type_indoor_fitness),
|
||||
INDOOR_ICE_SKATING(0x040000cc, R.string.activity_type_indoor_ice_skating),
|
||||
INDOOR_ICE_SKATING(0x040000cc, R.string.activity_type_indoor_ice_skating, R.drawable.ic_activity_ice_skating),
|
||||
JAI_ALAI(0x040000cd, R.string.activity_type_jai_alai),
|
||||
JUDO(0x040000ce, R.string.activity_type_judo),
|
||||
JUJITSU(0x040000cf, R.string.activity_type_jujitsu),
|
||||
@ -284,8 +284,8 @@ public enum ActivityKind {
|
||||
BODY_COMBAT(0x040000e5, R.string.activity_type_body_combat),
|
||||
PLAZA_DANCING(0x040000e6, R.string.activity_type_plaza_dancing),
|
||||
LASER_TAG(0x040000e7, R.string.activity_type_laser_tag),
|
||||
OBSTACLE_RACE(0x040000e8, R.string.activity_type_obstacle_race),
|
||||
BILLIARD_POOL(0x040000e9, R.string.activity_type_billiard_pool),
|
||||
OBSTACLE_RACE(0x040000e8, R.string.activity_type_obstacle_race, R.drawable.ic_activity_obstacle_race),
|
||||
BILLIARD_POOL(0x040000e9, R.string.activity_type_billiard_pool, R.drawable.ic_activity_billiard_pool),
|
||||
CANOEING(0x040000ea, R.string.activity_type_canoeing),
|
||||
WATER_SCOOTER(0x040000eb, R.string.activity_type_water_scooter),
|
||||
BOBSLEIGH(0x040000ec, R.string.activity_type_bobsleigh),
|
||||
|
@ -57,7 +57,7 @@ public class ActivitySummaryJsonSummary {
|
||||
summary.add("baseAltitude", item.getBaseAltitude(), UNIT_METERS);
|
||||
}
|
||||
|
||||
if (!summary.has("averageKMPaceSeconds") && !summary.has("averageSpeed") && summary.has("distanceMeters") && summary.has("activeSeconds")) {
|
||||
if (!summary.has("averageSpeed") && summary.has("distanceMeters") && summary.has("activeSeconds")) {
|
||||
double distance = summary.getNumber("distanceMeters", 0).doubleValue();
|
||||
double duration = summary.getNumber("activeSeconds", 1).doubleValue();
|
||||
summary.add("averageSpeed", distance / duration, UNIT_METERS_PER_SECOND);
|
||||
|
@ -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();
|
||||
}
|
@ -80,6 +80,8 @@ public interface DeviceService extends EventHandler {
|
||||
String ACTION_SET_LED_COLOR = PREFIX + ".action.set_led_color";
|
||||
String ACTION_POWER_OFF = PREFIX + ".action.power_off";
|
||||
String ACTION_CAMERA_STATUS_CHANGE = PREFIX + ".action.camera_status_change";
|
||||
String ACTION_REQUEST_MUSIC_LIST = PREFIX + ".action.request_music_list";
|
||||
String ACTION_REQUEST_MUSIC_OPERATION = PREFIX + ".action.request_music_operation";
|
||||
|
||||
String ACTION_SLEEP_AS_ANDROID = ".action.sleep_as_android";
|
||||
String EXTRA_SLEEP_AS_ANDROID_ACTION = "sleepasandroid_action";
|
||||
|
@ -79,8 +79,11 @@ 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.GarminForerunner55Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner620Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner955Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner965Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinct2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinct2SCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinct2SSolarCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinct2SolTacCoordinator;
|
||||
@ -218,16 +221,20 @@ import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaMhoC303Coor
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miscale.MiSmartScaleCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miscale.MiCompositionScaleCoordinator;
|
||||
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;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.EarStickCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoEncoAirCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qc35.QC35Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsT110Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.scannable.ScannableDeviceCoordinator;
|
||||
@ -429,6 +436,7 @@ public enum DeviceType {
|
||||
GARMIN_FENIX_7S(GarminFenix7SCoordinator.class),
|
||||
GARMIN_FENIX_7_PRO(GarminFenix7ProCoordinator.class),
|
||||
GARMIN_FENIX_8(GarminFenix8Coordinator.class),
|
||||
GARMIN_FORERUNNER_55(GarminForerunner55Coordinator.class),
|
||||
GARMIN_FORERUNNER_165(GarminForerunner165Coordinator.class),
|
||||
GARMIN_FORERUNNER_235(GarminForerunner235Coordinator.class),
|
||||
GARMIN_FORERUNNER_245(GarminForerunner245Coordinator.class),
|
||||
@ -439,11 +447,13 @@ public enum DeviceType {
|
||||
GARMIN_FORERUNNER_255S_MUSIC(GarminForerunner255SMusicCoordinator.class),
|
||||
GARMIN_FORERUNNER_265(GarminForerunner265Coordinator.class),
|
||||
GARMIN_FORERUNNER_265S(GarminForerunner265SCoordinator.class),
|
||||
GARMIN_FORERUNNER_620(GarminForerunner620Coordinator.class),
|
||||
GARMIN_FORERUNNER_955(GarminForerunner955Coordinator.class),
|
||||
GARMIN_FORERUNNER_965(GarminForerunner965Coordinator.class),
|
||||
GARMIN_SWIM_2(GarminSwim2Coordinator.class),
|
||||
GARMIN_INSTINCT(GarminInstinctCoordinator.class),
|
||||
GARMIN_INSTINCT_SOLAR(GarminInstinctSolarCoordinator.class),
|
||||
GARMIN_INSTINCT_2(GarminInstinct2Coordinator.class),
|
||||
GARMIN_INSTINCT_2S(GarminInstinct2SCoordinator.class),
|
||||
GARMIN_INSTINCT_2S_SOLAR(GarminInstinct2SSolarCoordinator.class),
|
||||
GARMIN_INSTINCT_2X_SOLAR(GarminInstinct2XSolarCoordinator.class),
|
||||
@ -538,6 +548,8 @@ public enum DeviceType {
|
||||
FLIPPER_ZERO(FlipperZeroCoordinator.class),
|
||||
SUPER_CARS(SuperCarsCoordinator.class),
|
||||
ASTEROIDOS(AsteroidOSDeviceCoordinator.class),
|
||||
OPPO_ENCO_AIR(OppoEncoAirCoordinator.class),
|
||||
REALME_BUDS_T110(RealmeBudsT110Coordinator.class),
|
||||
SOFLOW_SO6(SoFlowCoordinator.class),
|
||||
WITHINGS_STEEL_HR(WithingsSteelHRDeviceCoordinator.class),
|
||||
SONY_WENA_3(SonyWena3Coordinator.class),
|
||||
@ -548,6 +560,8 @@ public enum DeviceType {
|
||||
COLMI_R03(ColmiR03Coordinator.class),
|
||||
COLMI_R06(ColmiR06Coordinator.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),
|
||||
|
@ -63,6 +63,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.CameraActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.FindPhoneActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.musicmanager.MusicManagerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
@ -86,12 +87,16 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePref
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventWearState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceMusicData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceMusicUpdate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BatteryLevel;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusicPlaylist;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
@ -236,7 +241,12 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
handleGBDeviceEvent((GBDeviceEventWearState) deviceEvent);
|
||||
} else if (deviceEvent instanceof GBDeviceEventSleepStateDetection) {
|
||||
handleGBDeviceEvent((GBDeviceEventSleepStateDetection) deviceEvent);
|
||||
} else if (deviceEvent instanceof GBDeviceMusicData) {
|
||||
handleGBDeviceEvent((GBDeviceMusicData) deviceEvent);
|
||||
} else if (deviceEvent instanceof GBDeviceMusicUpdate) {
|
||||
handleGBDeviceEvent((GBDeviceMusicUpdate) deviceEvent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void handleGBDeviceEvent(GBDeviceEventSilentMode deviceEvent) {
|
||||
@ -751,6 +761,53 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
handleDeviceAction(actionOnUnwear, broadcastMessage);
|
||||
}
|
||||
|
||||
private void handleGBDeviceEvent(GBDeviceMusicData deviceEvent) {
|
||||
Context context = getContext();
|
||||
LOG.info("Got event for ACTION_MUSIC_DATA");
|
||||
|
||||
Intent intent = new Intent(MusicManagerActivity.ACTION_MUSIC_DATA);
|
||||
|
||||
intent.putExtra("type", deviceEvent.type);
|
||||
|
||||
if(deviceEvent.list != null) {
|
||||
ArrayList<GBDeviceMusic> list = new ArrayList<>(deviceEvent.list);
|
||||
intent.putExtra("musicList", list);
|
||||
}
|
||||
|
||||
if(deviceEvent.playlists != null) {
|
||||
ArrayList<GBDeviceMusicPlaylist> list = new ArrayList<>(deviceEvent.playlists);
|
||||
intent.putExtra("musicPlaylist", list);
|
||||
}
|
||||
|
||||
if(!TextUtils.isEmpty(deviceEvent.deviceInfo)) {
|
||||
intent.putExtra("deviceInfo", deviceEvent.deviceInfo);
|
||||
}
|
||||
|
||||
if(deviceEvent.maxMusicCount > 0) {
|
||||
intent.putExtra("maxMusicCount", deviceEvent.maxMusicCount);
|
||||
}
|
||||
if(deviceEvent.maxPlaylistCount > 0) {
|
||||
intent.putExtra("maxPlaylistCount", deviceEvent.maxPlaylistCount);
|
||||
}
|
||||
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
private void handleGBDeviceEvent(GBDeviceMusicUpdate deviceEvent) {
|
||||
Context context = getContext();
|
||||
LOG.info("Got event for ACTION_MUSIC_UPDATE");
|
||||
|
||||
Intent intent = new Intent(MusicManagerActivity.ACTION_MUSIC_UPDATE);
|
||||
|
||||
intent.putExtra("success", deviceEvent.success);
|
||||
intent.putExtra("operation", deviceEvent.operation);
|
||||
intent.putExtra("playlistIndex", deviceEvent.playlistIndex);
|
||||
intent.putExtra("playlistName", deviceEvent.playlistName);
|
||||
intent.putExtra("musicIds", deviceEvent.musicIds);
|
||||
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
private StoreDataTask createStoreTask(String task, Context context, GBDeviceEventBatteryInfo deviceEvent) {
|
||||
return new StoreDataTask(task, context, deviceEvent);
|
||||
}
|
||||
@ -1233,4 +1290,10 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
|
||||
@Override
|
||||
public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) {}
|
||||
|
||||
@Override
|
||||
public void onMusicListReq() {}
|
||||
|
||||
@Override
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {}
|
||||
}
|
||||
|
@ -108,7 +108,6 @@ public abstract class AbstractHeadphoneDeviceSupport extends AbstractSerialDevic
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
LOG.warn("ONSENDCONFIGURATION");
|
||||
if (PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE.equals(config)) {
|
||||
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
|
||||
gbTextToSpeech.setAudioFocus(prefs.getBoolean(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE, false) ?
|
||||
|
@ -87,6 +87,7 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.TinyWeatherForecastGe
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid.SleepAsAndroidReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
@ -1137,6 +1138,16 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
}
|
||||
deviceSupport.onCameraStatusChange(event, filename);
|
||||
break;
|
||||
case ACTION_REQUEST_MUSIC_LIST:
|
||||
deviceSupport.onMusicListReq();
|
||||
break;
|
||||
case ACTION_REQUEST_MUSIC_OPERATION:
|
||||
int operation = intentCopy.getIntExtra("operation", -1);
|
||||
int playlistIndex = intentCopy.getIntExtra("playlistIndex", -1);
|
||||
String playlistName = intentCopy.getStringExtra("playlistName");
|
||||
ArrayList<Integer> musics = (ArrayList<Integer>) intentCopy.getSerializableExtra("musicIds");
|
||||
deviceSupport.onMusicOperation(operation, playlistIndex, playlistName, musics);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,7 @@ import java.util.UUID;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
@ -524,4 +525,20 @@ public class ServiceDeviceSupport implements DeviceSupport {
|
||||
}
|
||||
delegate.onCameraStatusChange(event, filename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicListReq() {
|
||||
if (checkBusy("music list request")) {
|
||||
return;
|
||||
}
|
||||
delegate.onMusicListReq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
if (checkBusy("music operation")) {
|
||||
return;
|
||||
}
|
||||
delegate.onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -50,6 +50,10 @@ public class BandWBLEProfile<T extends AbstractBTLEDeviceSupport> extends Abstra
|
||||
sendRequest(builder, (byte) 0x03, (byte) 0x03);
|
||||
}
|
||||
|
||||
public void requestWearSensorEnabled(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x0a, (byte) 0x01);
|
||||
}
|
||||
|
||||
public void setAncModeState(final TransactionBuilder builder, final boolean mode) throws IOException {
|
||||
BandWPSeriesRequest req = new BandWPSeriesRequest((byte) 0x03, (byte) 0x02).addToPayload(mode ? ANC_MODE_ON : ANC_MODE_OFF);
|
||||
builder.write(getCharacteristic(UUID_RPC_REQUEST_CHARACTERISTIC), req.finishAndGetBytes());
|
||||
@ -65,6 +69,11 @@ public class BandWBLEProfile<T extends AbstractBTLEDeviceSupport> extends Abstra
|
||||
builder.write(getCharacteristic(UUID_RPC_REQUEST_CHARACTERISTIC), req.finishAndGetBytes());
|
||||
}
|
||||
|
||||
public void setWearSensorEnabled(final TransactionBuilder builder, final boolean mode) throws IOException {
|
||||
BandWPSeriesRequest req = new BandWPSeriesRequest((byte) 0x0a, (byte) 0x02).addToPayload(mode);
|
||||
builder.write(getCharacteristic(UUID_RPC_REQUEST_CHARACTERISTIC), req.finishAndGetBytes());
|
||||
}
|
||||
|
||||
private void sendRequest(final TransactionBuilder builder, byte namespace, byte commandID) {
|
||||
BandWPSeriesRequest req;
|
||||
try {
|
||||
|
@ -4,6 +4,7 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANDW_PSERIES_GUI_VPT_LEVEL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANDW_PSERIES_VPT_ENABLED;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANDW_PSERIES_VPT_LEVEL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEAR_SENSOR_TOGGLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.BATTERY_UNKNOWN;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries.BandWBLEProfile.ANC_MODE_ON;
|
||||
|
||||
@ -78,6 +79,7 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
BandWBLEProfile.requestAncModeState(builder);
|
||||
BandWBLEProfile.requestVptEnabled(builder);
|
||||
BandWBLEProfile.requestVptLevel(builder);
|
||||
BandWBLEProfile.requestWearSensorEnabled(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
@ -129,6 +131,12 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
if (response.commandId == 0x17) {
|
||||
return handleBatteryLevels(response);
|
||||
}
|
||||
} else if (response.namespace == 0x0a) {
|
||||
if (response.commandId == 0x01) {
|
||||
return handleGetWearSensorEnabledResponse(response);
|
||||
} else if (response.commandId == 0x02) {
|
||||
return getBooleanResponseStatus(response);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -168,6 +176,24 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleGetWearSensorEnabledResponse(BandWPSeriesResponse response) {
|
||||
if (!response.messageType.hasPayload) {
|
||||
GB.toast("No payload in response!", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
boolean wearSensorEnabled;
|
||||
try {
|
||||
wearSensorEnabled = response.getPayloadBoolean();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Failed to unpack wear sensor status from payload " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
|
||||
editor.putBoolean(PREF_WEAR_SENSOR_TOGGLE, wearSensorEnabled);
|
||||
editor.apply();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleFirmwareVersionResponse(BandWPSeriesResponse response) {
|
||||
String firmwareString = response.getPayloadString();
|
||||
if (firmwareString == null) {
|
||||
@ -249,6 +275,10 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
BandWBLEProfile.setVptLevel(builder, level - 1);
|
||||
}
|
||||
break;
|
||||
case PREF_WEAR_SENSOR_TOGGLE:
|
||||
boolean wearSensorEnabled = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(PREF_WEAR_SENSOR_TOGGLE, true);
|
||||
BandWBLEProfile.setWearSensorEnabled(builder, wearSensorEnabled);
|
||||
break;
|
||||
}
|
||||
performImmediately(builder);
|
||||
} catch (IOException e) {
|
||||
|
@ -77,4 +77,8 @@ public class BandWPSeriesResponse {
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
public boolean getPayloadBoolean() throws IOException{
|
||||
return payloadUnpacker.unpackBoolean();
|
||||
}
|
||||
}
|
||||
|
@ -300,7 +300,7 @@ public class CasioGBX100DeviceSupport extends Casio2C2DSupport implements Shared
|
||||
// If not a call or email, check the sender and if null, promote the title and message preview
|
||||
// as subtitle
|
||||
if (showMessagePreview && icon != CasioConstants.CATEGORY_INCOMING_CALL && icon != CasioConstants.CATEGORY_EMAIL) {
|
||||
if (!StringUtils.isNullOrEmpty(sender)) {
|
||||
if (StringUtils.isNullOrEmpty(sender)) {
|
||||
// Shift title to sender slot
|
||||
sender = title;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes
|
||||
public class FieldDefinitionTemperature extends FieldDefinition {
|
||||
|
||||
public FieldDefinitionTemperature(int localNumber, int size, BaseType baseType, String name) {
|
||||
// #4313 - We do a "wrong" conversion to celsius on purpose
|
||||
super(localNumber, size, baseType, name, 1, -273);
|
||||
}
|
||||
|
||||
|
@ -273,7 +273,8 @@ public class WeatherHandler {
|
||||
return new WeatherValue(kelvin, "KELVIN");
|
||||
case "CELSIUS":
|
||||
default:
|
||||
return new WeatherValue(kelvin - 273.15, "CELSIUS");
|
||||
// #4313 - We do a "wrong" conversion to celsius on purpose
|
||||
return new WeatherValue(kelvin - 273, "CELSIUS");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ import java.util.UUID;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
|
||||
@ -198,4 +199,14 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
|
||||
public void onTestNewFunction() {
|
||||
supportProvider.onTestNewFunction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicListReq() {
|
||||
supportProvider.onMusicListReq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
supportProvider.onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import java.util.UUID;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
|
||||
@ -214,4 +215,14 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport {
|
||||
public boolean getSendWriteRequestResponse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicListReq() {
|
||||
supportProvider.onMusicListReq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
supportProvider.onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,28 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceMusicData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceMusicUpdate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiMusicUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusicPlaylist;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetMusicInfoParams;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetMusicList;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetMusicPlaylist;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetMusicPlaylistMusics;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMusicOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendUploadMusicFileInfoResponse;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class HuaweiMusicManager {
|
||||
static Logger LOG = LoggerFactory.getLogger(HuaweiMusicManager.class);
|
||||
@ -125,4 +142,236 @@ public class HuaweiMusicManager {
|
||||
LOG.error("Could not send sendUploadMusicFileInfoResponse", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean syncMusicData = false;
|
||||
private int frameCount = 0;
|
||||
private int endFrame = 65535;
|
||||
private int currentFrame = 0;
|
||||
|
||||
|
||||
public void startSyncMusicData() {
|
||||
syncMusicData = true;
|
||||
try {
|
||||
GetMusicInfoParams getMusicInfoParams = new GetMusicInfoParams(this.support);
|
||||
getMusicInfoParams.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Get music info: {}", e.getMessage());
|
||||
syncMusicData = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void syncMusicList() {
|
||||
if (!syncMusicData) {
|
||||
this.currentFrame = 0;
|
||||
return;
|
||||
}
|
||||
int count = this.frameCount;
|
||||
if (support.getHuaweiCoordinator().supportsMoreMusic()) {
|
||||
count = Math.min(this.frameCount, 250);
|
||||
}
|
||||
if (this.currentFrame < count) {
|
||||
try {
|
||||
GetMusicList getMusicList = new GetMusicList(this.support, this.currentFrame, this.endFrame);
|
||||
getMusicList.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Get music list: {}", e.getMessage());
|
||||
endMusicListSync();
|
||||
}
|
||||
} else {
|
||||
endMusicListSync();
|
||||
}
|
||||
}
|
||||
|
||||
private void endMusicListSync() {
|
||||
this.currentFrame = 0;
|
||||
try {
|
||||
GetMusicPlaylist getMusicPlaylist = new GetMusicPlaylist(this.support);
|
||||
getMusicPlaylist.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Get music playlist: {}", e.getMessage());
|
||||
endMusicPlaylistSync();
|
||||
}
|
||||
}
|
||||
|
||||
private void endMusicPlaylistSync() {
|
||||
this.currentPlaylistIndex = 0;
|
||||
this.currentPlaylistFrame = 0;
|
||||
tempPlaylistMusic.clear();
|
||||
|
||||
musicPlaylistMusicSync();
|
||||
}
|
||||
|
||||
private final List<MusicControl.MusicPlaylists.Response.PlaylistData> devicePlaylists = new ArrayList<>();
|
||||
|
||||
private int currentPlaylistIndex = 0;
|
||||
private int currentPlaylistFrame = 0;
|
||||
private final List<List<Integer>> tempPlaylistMusic = new ArrayList<>();
|
||||
|
||||
private void musicPlaylistMusicSync() {
|
||||
if (this.currentPlaylistIndex < devicePlaylists.size()) {
|
||||
MusicControl.MusicPlaylists.Response.PlaylistData playlist = devicePlaylists.get(this.currentPlaylistIndex);
|
||||
syncPlaylistMusicsOne(playlist.id, playlist.frameCount);
|
||||
} else {
|
||||
musicPlaylistMusicDone();
|
||||
}
|
||||
}
|
||||
|
||||
private void syncPlaylistMusicsOne(int id, int frameCount) {
|
||||
if (this.currentPlaylistFrame < frameCount) {
|
||||
try {
|
||||
GetMusicPlaylistMusics getMusicPlaylistMusics = new GetMusicPlaylistMusics(this.support, id, this.currentPlaylistFrame);
|
||||
getMusicPlaylistMusics.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Get music playlist musics: {}", e.getMessage());
|
||||
musicPlaylistMusicDone();
|
||||
}
|
||||
} else {
|
||||
syncPlayListMusicIndexDone(id, frameCount);
|
||||
}
|
||||
}
|
||||
|
||||
public void syncNextPlaylistMusicIndex() {
|
||||
this.currentPlaylistFrame++;
|
||||
MusicControl.MusicPlaylists.Response.PlaylistData playlist = devicePlaylists.get(this.currentPlaylistIndex);
|
||||
syncPlaylistMusicsOne(playlist.id, playlist.frameCount);
|
||||
}
|
||||
|
||||
private void syncPlayListMusicIndexDone(int id, int frameCount) {
|
||||
MusicControl.MusicPlaylists.Response.PlaylistData playlist = devicePlaylists.get(this.currentPlaylistIndex);
|
||||
|
||||
ArrayList<Integer> musics = new ArrayList<>();
|
||||
if (this.tempPlaylistMusic.size() == frameCount) {
|
||||
for (int i = 0; i < frameCount; i++) {
|
||||
musics.addAll(this.tempPlaylistMusic.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
GBDeviceMusicPlaylist pl = new GBDeviceMusicPlaylist(playlist.id, playlist.name, musics);
|
||||
List<GBDeviceMusicPlaylist> list = new ArrayList<>();
|
||||
list.add(pl);
|
||||
sendMusicPlaylist(list);
|
||||
this.currentPlaylistIndex++;
|
||||
this.currentPlaylistFrame = 0;
|
||||
this.tempPlaylistMusic.clear();
|
||||
musicPlaylistMusicSync();
|
||||
}
|
||||
|
||||
private void musicPlaylistMusicDone() {
|
||||
this.currentPlaylistIndex = 0;
|
||||
this.currentPlaylistFrame = 0;
|
||||
this.tempPlaylistMusic.clear();
|
||||
|
||||
this.syncMusicData = false;
|
||||
sendMusicSyncDone();
|
||||
}
|
||||
|
||||
public void onMusicMusicInfoParams(HuaweiMusicUtils.MusicCapabilities capabilities, int frameCount, List<HuaweiMusicUtils.PageStruct> pageStruct) {
|
||||
//TODO: research and use pageStruct. It may/should be used to retrieve music data from devices by pages.
|
||||
// without it list can be incomplete, but I can't confirm this.
|
||||
LOG.info("FrameCount: {}, pageStruct: {}", frameCount, pageStruct);
|
||||
support.getHuaweiCoordinator().setMusicInfoParams(capabilities);
|
||||
if(syncMusicData) {
|
||||
this.frameCount = frameCount;
|
||||
this.currentFrame = 0;
|
||||
this.endFrame = 65535;
|
||||
String formats = null;
|
||||
if(capabilities.supportedFormats != null) {
|
||||
formats = String.join(",", capabilities.supportedFormats);
|
||||
}
|
||||
int maxPlaylistCount = 0;
|
||||
if(support.getCoordinator().getHuaweiCoordinator().getExtendedMusicInfoParams() != null) {
|
||||
maxPlaylistCount = support.getCoordinator().getHuaweiCoordinator().getExtendedMusicInfoParams().maxPlaylistCount;
|
||||
}
|
||||
sendMusicSyncStart(support.getContext().getString(R.string.music_huawei_device_info, formats, capabilities.availableSpace), capabilities.maxMusicCount, maxPlaylistCount);
|
||||
syncMusicList();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMusicSyncStart(final String info, int maxMusicCount, int maxPlaylistCount) {
|
||||
final GBDeviceMusicData musicListCmd = new GBDeviceMusicData();
|
||||
musicListCmd.type = 1;
|
||||
musicListCmd.deviceInfo = info;
|
||||
musicListCmd.maxMusicCount = maxMusicCount;
|
||||
musicListCmd.maxPlaylistCount = maxPlaylistCount;
|
||||
support.evaluateGBDeviceEvent(musicListCmd);
|
||||
}
|
||||
|
||||
|
||||
private void sendMusicList(List<GBDeviceMusic> list) {
|
||||
final GBDeviceMusicData musicListCmd = new GBDeviceMusicData();
|
||||
musicListCmd.type = 2;
|
||||
musicListCmd.list = list;
|
||||
support.evaluateGBDeviceEvent(musicListCmd);
|
||||
}
|
||||
|
||||
private void sendMusicPlaylist(List<GBDeviceMusicPlaylist> list) {
|
||||
final GBDeviceMusicData musicListCmd = new GBDeviceMusicData();
|
||||
musicListCmd.type = 2;
|
||||
musicListCmd.playlists = list;
|
||||
support.evaluateGBDeviceEvent(musicListCmd);
|
||||
}
|
||||
|
||||
private void sendMusicSyncDone() {
|
||||
final GBDeviceMusicData musicListCmd = new GBDeviceMusicData();
|
||||
musicListCmd.type = 10;
|
||||
support.evaluateGBDeviceEvent(musicListCmd);
|
||||
}
|
||||
|
||||
public void onMusicListResponse(int startFrame, int endFrame, List<GBDeviceMusic> list) {
|
||||
sendMusicList(list);
|
||||
if (support.getHuaweiCoordinator().supportsMoreMusic() || !(endFrame == this.endFrame || list.size() == 1)) {
|
||||
if (list.size() == 2) {
|
||||
this.endFrame = list.get(1).getId();
|
||||
}
|
||||
this.currentFrame++;
|
||||
syncMusicList();
|
||||
return;
|
||||
}
|
||||
endMusicListSync();
|
||||
}
|
||||
|
||||
public void onMusicPlaylistResponse(List<MusicControl.MusicPlaylists.Response.PlaylistData> playlists) {
|
||||
this.devicePlaylists.clear();
|
||||
for(MusicControl.MusicPlaylists.Response.PlaylistData pl: playlists) {
|
||||
if(pl.id != 0) {
|
||||
this.devicePlaylists.add(pl);
|
||||
}
|
||||
}
|
||||
endMusicPlaylistSync();
|
||||
}
|
||||
|
||||
public void onMusicPlaylistMusics(int id, int index, List<Integer> musicIds) {
|
||||
this.tempPlaylistMusic.add(musicIds);
|
||||
syncNextPlaylistMusicIndex();
|
||||
}
|
||||
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
LOG.info("music operation: {}", operation);
|
||||
try {
|
||||
SendMusicOperation sendMusicOperation = new SendMusicOperation(this.support, operation, playlistIndex, playlistName, musicIds);
|
||||
sendMusicOperation.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("SendMusicOperation: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void onMusicOperationResponse(int resultCode, int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
|
||||
boolean success = true;
|
||||
if (resultCode != 0x000186A0) {
|
||||
GB.toast(support.getContext(), support.getContext().getString(R.string.music_error), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
success = false;
|
||||
}
|
||||
|
||||
LOG.info("music operation response: {} {}", operation, success);
|
||||
final GBDeviceMusicUpdate updateCmd = new GBDeviceMusicUpdate();
|
||||
updateCmd.success = success;
|
||||
updateCmd.operation = operation;
|
||||
updateCmd.playlistIndex = playlistIndex;
|
||||
updateCmd.playlistName = playlistName;
|
||||
updateCmd.musicIds = musicIds;
|
||||
|
||||
support.evaluateGBDeviceEvent(updateCmd);
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceMusicData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinator;
|
||||
@ -124,6 +125,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Send
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFitnessUserInfoRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsDataRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendHeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendRunPaceConfigRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendSetContactsRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyHeartRateCapabilityRequest;
|
||||
@ -833,6 +835,7 @@ public class HuaweiSupportProvider {
|
||||
initRequestQueue.add(new SendFitnessUserInfoRequest(this));
|
||||
initRequestQueue.add(new SendRunPaceConfigRequest(this));
|
||||
initRequestQueue.add(new SendDeviceReportThreshold(this));
|
||||
initRequestQueue.add(new SendHeartRateZonesConfig(this));
|
||||
initRequestQueue.add(new SetMediumToStrengthThresholdRequest(this));
|
||||
initRequestQueue.add(new SendFitnessGoalRequest(this));
|
||||
initRequestQueue.add(new GetNotificationCapabilitiesRequest(this));
|
||||
@ -2529,4 +2532,12 @@ public class HuaweiSupportProvider {
|
||||
callback
|
||||
), true);
|
||||
}
|
||||
|
||||
public void onMusicListReq() {
|
||||
getHuaweiMusicManager().startSyncMusicData();
|
||||
}
|
||||
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
getHuaweiMusicManager().onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,6 @@ public class GetMusicInfoParams extends Request {
|
||||
throw new Request.ResponseTypeMismatchException(receivedPacket, MusicControl.MusicInfoParams.Response.class);
|
||||
|
||||
MusicControl.MusicInfoParams.Response resp = (MusicControl.MusicInfoParams.Response)(receivedPacket);
|
||||
supportProvider.getHuaweiCoordinator().setMusicInfoParams(resp.params);
|
||||
supportProvider.getHuaweiMusicManager().onMusicMusicInfoParams(resp.params, resp.frameCount, resp.pageStruct);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetMusicList extends Request {
|
||||
private final Logger LOG = LoggerFactory.getLogger(GetMusicList.class);
|
||||
|
||||
private final int startFrame;
|
||||
private final int endFrame;
|
||||
|
||||
public GetMusicList(HuaweiSupportProvider support, int startFrame, int endFrame) {
|
||||
super(support);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = MusicControl.MusicList.id;
|
||||
this.startFrame = startFrame;
|
||||
this.endFrame = endFrame;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new MusicControl.MusicList.Request(paramsProvider, (short) this.startFrame, (short) this.endFrame).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws Request.ResponseParseException {
|
||||
LOG.info("MusicControl.MusicList processResponse");
|
||||
if (!(receivedPacket instanceof MusicControl.MusicList.Response))
|
||||
throw new Request.ResponseTypeMismatchException(receivedPacket, MusicControl.MusicList.Response.class);
|
||||
|
||||
MusicControl.MusicList.Response resp = (MusicControl.MusicList.Response) (receivedPacket);
|
||||
supportProvider.getHuaweiMusicManager().onMusicListResponse(resp.startFrame, resp.endIndex, resp.musicList);
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetMusicPlaylist extends Request {
|
||||
private final Logger LOG = LoggerFactory.getLogger(GetMusicPlaylist.class);
|
||||
|
||||
public GetMusicPlaylist(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = MusicControl.MusicPlaylists.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new MusicControl.MusicPlaylists.Request(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws Request.ResponseParseException {
|
||||
LOG.info("MusicControl.MusicPlaylists processResponse");
|
||||
if (!(receivedPacket instanceof MusicControl.MusicPlaylists.Response))
|
||||
throw new Request.ResponseTypeMismatchException(receivedPacket, MusicControl.MusicPlaylists.Response.class);
|
||||
|
||||
MusicControl.MusicPlaylists.Response resp = (MusicControl.MusicPlaylists.Response) (receivedPacket);
|
||||
supportProvider.getHuaweiMusicManager().onMusicPlaylistResponse(resp.playlists);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetMusicPlaylistMusics extends Request {
|
||||
private final Logger LOG = LoggerFactory.getLogger(GetMusicPlaylistMusics.class);
|
||||
|
||||
private final int playlist;
|
||||
private final int index;
|
||||
|
||||
public GetMusicPlaylistMusics(HuaweiSupportProvider support, int playlist, int index) {
|
||||
super(support);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = MusicControl.MusicPlaylistMusics.id;
|
||||
this.playlist = playlist;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new MusicControl.MusicPlaylistMusics.Request(paramsProvider, (short) playlist, (short) index).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws Request.ResponseParseException {
|
||||
LOG.info("MusicControl.GetMusicPlaylistMusics processResponse");
|
||||
if (!(receivedPacket instanceof MusicControl.MusicPlaylistMusics.Response))
|
||||
throw new Request.ResponseTypeMismatchException(receivedPacket, MusicControl.MusicPlaylistMusics.Response.class);
|
||||
|
||||
MusicControl.MusicPlaylistMusics.Response resp = (MusicControl.MusicPlaylistMusics.Response) (receivedPacket);
|
||||
supportProvider.getHuaweiMusicManager().onMusicPlaylistMusics(resp.id, resp.index, resp.musicIds);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/* Copyright (C) 2024 Martin.JM
|
||||
|
||||
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.service.devices.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendHeartRateZonesConfig extends Request {
|
||||
|
||||
public SendHeartRateZonesConfig(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = supportProvider.getHuaweiCoordinator().supportsExtendedHeartRateZones() ?
|
||||
FitnessData.HeartRateZoneConfigPacket.id_extended :
|
||||
FitnessData.HeartRateZoneConfigPacket.id_simple;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean requestSupported() {
|
||||
return
|
||||
!supportProvider.getHuaweiCoordinator().supportsTrack() && // In this case it uses P2P
|
||||
supportProvider.getHuaweiCoordinator().supportsHeartRateZones();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
HeartRateZonesConfig heartRateZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_UPRIGHT, new ActivityUser().getAge());
|
||||
if (supportProvider.getHuaweiCoordinator().supportsExtendedHeartRateZones()) {
|
||||
return FitnessData.HeartRateZoneConfigPacket.Request.requestExtended(paramsProvider, heartRateZonesConfig).serialize();
|
||||
} else {
|
||||
return FitnessData.HeartRateZoneConfigPacket.Request.requestSimple(paramsProvider, heartRateZonesConfig).serialize();
|
||||
}
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendMusicOperation extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendMusicOperation.class);
|
||||
|
||||
private final int operation;
|
||||
private final int playlistIndex;
|
||||
private final String playlistName;
|
||||
private final ArrayList<Integer> musicIds;
|
||||
|
||||
|
||||
public SendMusicOperation(HuaweiSupportProvider support, int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
super(support);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = MusicControl.MusicOperation.id;
|
||||
this.operation = operation;
|
||||
this.playlistIndex = playlistIndex;
|
||||
this.playlistName = playlistName;
|
||||
this.musicIds = musicIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new MusicControl.MusicOperation.Request(paramsProvider, operation, playlistIndex, playlistName, musicIds).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseTypeMismatchException {
|
||||
LOG.debug("handle Music Operation");
|
||||
if (!(receivedPacket instanceof MusicControl.MusicOperation.Response))
|
||||
throw new Request.ResponseTypeMismatchException(receivedPacket, MusicControl.MusicOperation.Response.class);
|
||||
|
||||
MusicControl.MusicOperation.Response resp = (MusicControl.MusicOperation.Response) (receivedPacket);
|
||||
supportProvider.getHuaweiMusicManager().onMusicOperationResponse(resp.resultCode, resp.operation, resp.playlistIndex, resp.playlistName, resp.musicIds);
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user