mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-26 20:06:52 +01:00
Compare commits
61 Commits
3670eb6e70
...
769c592940
Author | SHA1 | Date | |
---|---|---|---|
|
769c592940 | ||
|
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 | ||
|
ce32ac7272 | ||
|
2a865fe498 | ||
|
f3185f1acb | ||
|
6bb93bef89 | ||
|
7c1d44fcd3 | ||
|
a2323ce845 | ||
|
5a0f1e46db | ||
|
126102aa05 | ||
|
f0ffc0e165 | ||
|
82e3a86350 | ||
|
1882ee947e | ||
|
ae84678de8 | ||
|
68caf6a60f | ||
|
d53971c881 | ||
|
46dd45cb4e | ||
|
cd068abdd3 | ||
|
aa2b6d142a | ||
|
d9cc15e3c5 | ||
|
a7c19c8190 | ||
|
d038c589c1 | ||
|
8df3dac11c | ||
|
a12b56fb37 | ||
|
2315b56ff8 | ||
|
ed1f1735c5 | ||
|
292a5d11b5 | ||
|
a6053eda77 | ||
|
cbd9a7b8af | ||
|
29866c2b41 | ||
|
1e0117727e | ||
|
3a152978d7 | ||
|
7f9c571d63 | ||
|
16b12e099f | ||
|
dae338fdc5 | ||
|
a79cd94de9 |
@ -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>
|
||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,6 +1,21 @@
|
||||
### Changelog
|
||||
|
||||
#### Next version (WIP)
|
||||
#### 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
|
||||
* Initial support for Colmi R02/R03/R06/R10 smart rings
|
||||
|
@ -54,7 +54,7 @@ public class GBDaoGenerator {
|
||||
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
final Schema schema = new Schema(85, MAIN_PACKAGE + ".entities");
|
||||
final Schema schema = new Schema(86, MAIN_PACKAGE + ".entities");
|
||||
|
||||
Entity userAttributes = addUserAttributes(schema);
|
||||
Entity user = addUserInfo(schema, userAttributes);
|
||||
@ -153,6 +153,9 @@ public class GBDaoGenerator {
|
||||
addHuaweiWorkoutPaceSample(schema, huaweiWorkoutSummary);
|
||||
addHuaweiWorkoutSwimSegmentsSample(schema, huaweiWorkoutSummary);
|
||||
|
||||
Entity huaweiDictData = addHuaweiDictData(schema, user, device);
|
||||
addHuaweiDictDataValues(schema, huaweiDictData);
|
||||
|
||||
addCalendarSyncState(schema, device);
|
||||
addAlarms(schema, user, device);
|
||||
addReminders(schema, user, device);
|
||||
@ -1333,7 +1336,7 @@ public class GBDaoGenerator {
|
||||
activitySample.addIntProperty("distance").notNull().codeBeforeGetter(
|
||||
"@Override\n" +
|
||||
" public int getDistanceCm() {\n" +
|
||||
" return getDistance() * 100;\n" +
|
||||
" return getDistance() == HuaweiActivitySample.NOT_MEASURED ? HuaweiActivitySample.NOT_MEASURED : getDistance() * 100;\n" +
|
||||
" }\n"
|
||||
);
|
||||
activitySample.addIntProperty("spo").notNull();
|
||||
@ -1468,6 +1471,42 @@ public class GBDaoGenerator {
|
||||
return workoutSwimSegmentsSample;
|
||||
}
|
||||
|
||||
private static Entity addHuaweiDictData(Schema schema, Entity user, Entity device) {
|
||||
Entity dictData = addEntity(schema, "HuaweiDictData");
|
||||
|
||||
dictData.setJavaDoc("Contains Huawei Dict Data");
|
||||
|
||||
dictData.addLongProperty("dictId").primaryKey().autoincrement();
|
||||
|
||||
Property deviceId = dictData.addLongProperty("deviceId").notNull().getProperty();
|
||||
dictData.addToOne(device, deviceId);
|
||||
Property userId = dictData.addLongProperty("userId").notNull().getProperty();
|
||||
dictData.addToOne(user, userId);
|
||||
|
||||
dictData.addIntProperty("dictClass").notNull();
|
||||
dictData.addLongProperty("startTimestamp").notNull();
|
||||
dictData.addLongProperty("endTimestamp");
|
||||
dictData.addLongProperty("modifyTimestamp");
|
||||
|
||||
return dictData;
|
||||
}
|
||||
|
||||
private static Entity addHuaweiDictDataValues(Schema schema, Entity summaryEntity) {
|
||||
Entity dictDataValues = addEntity(schema, "HuaweiDictDataValues");
|
||||
|
||||
dictDataValues.setJavaDoc("Contains Huawei Dict data values");
|
||||
|
||||
Property id = dictDataValues.addLongProperty("dictId").primaryKey().notNull().getProperty();
|
||||
dictDataValues.addToOne(summaryEntity, id);
|
||||
|
||||
dictDataValues.addIntProperty("dictType").notNull().primaryKey();
|
||||
dictDataValues.addByteProperty("tag").notNull().primaryKey();
|
||||
dictDataValues.addByteArrayProperty("value");
|
||||
|
||||
return dictDataValues;
|
||||
}
|
||||
|
||||
|
||||
private static void addTemperatureProperties(Entity activitySample) {
|
||||
activitySample.addFloatProperty(SAMPLE_TEMPERATURE).notNull().codeBeforeGetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_TEMPERATURE_TYPE).notNull().codeBeforeGetter(OVERRIDE);
|
||||
|
@ -79,8 +79,8 @@ android {
|
||||
minSdkVersion 21
|
||||
|
||||
// Note: always bump BOTH versionCode and versionName!
|
||||
versionName "0.81.0"
|
||||
versionCode 232
|
||||
versionName "0.82.1"
|
||||
versionCode 234
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
buildConfigField "String", "GIT_HASH_SHORT", "\"${getGitHashShort()}\""
|
||||
buildConfigField "boolean", "INTERNET_ACCESS", "false"
|
||||
@ -198,13 +198,13 @@ 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.1.4'
|
||||
implementation "androidx.camera:camera-core:1.3.4"
|
||||
implementation "androidx.camera:camera-camera2:1.3.4"
|
||||
implementation 'androidx.camera:camera-view:1.3.4'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.3.4'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation "androidx.camera:camera-core:1.4.0"
|
||||
implementation "androidx.camera:camera-camera2:1.4.0"
|
||||
implementation 'androidx.camera:camera-view:1.4.0'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.4.0'
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "org.mockito:mockito-core:5.14.2"
|
||||
@ -219,7 +219,7 @@ dependencies {
|
||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||
implementation "androidx.palette:palette:1.0.0"
|
||||
implementation "androidx.activity:activity:1.9.3"
|
||||
implementation "androidx.fragment:fragment:1.8.4"
|
||||
implementation "androidx.fragment:fragment:1.8.5"
|
||||
implementation "androidx.viewpager2:viewpager2:1.1.0"
|
||||
|
||||
// Not latest version because of https://github.com/material-components/material-components-android/issues/3924
|
||||
@ -246,6 +246,7 @@ dependencies {
|
||||
implementation 'com.github.wax911.android-emojify:gson:1.9.4'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:4.28.2'
|
||||
implementation 'com.android.volley:volley:1.2.1'
|
||||
implementation 'org.msgpack:msgpack-core:0.9.8'
|
||||
|
||||
// Bouncy Castle is included directly in GB, to avoid pulling the entire dependency
|
||||
// It's included in the org.bouncycastle.shaded package, to fix conflicts with roboelectric
|
||||
|
@ -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"
|
||||
|
@ -152,6 +152,11 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
|
||||
return getAllSamples(db, device, tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<? extends ActivitySample> getSamplesHighRes(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
return getAllSamplesHighRes(db, device, tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(Chart<?> chart) {
|
||||
List<LegendEntry> legendEntries = new ArrayList<>(5);
|
||||
@ -231,9 +236,12 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
|
||||
|
||||
private DefaultChartsData<LineData> buildChartFromSamples(DBHandler handler) {
|
||||
final List<? extends ActivitySample> samples = getAllSamples(handler, gbDevice, startTime, endTime);
|
||||
final List<? extends ActivitySample> highResSamples = getAllSamplesHighRes(handler, gbDevice, startTime, endTime);
|
||||
|
||||
try {
|
||||
return refresh(gbDevice, samples);
|
||||
if (highResSamples == null)
|
||||
return refresh(gbDevice, samples);
|
||||
return refresh(gbDevice, samples, highResSamples);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Unable to get charts data right now", e);
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import com.github.mikephil.charting.data.LineDataSet;
|
||||
import com.github.mikephil.charting.formatter.ValueFormatter;
|
||||
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
|
||||
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -180,14 +181,28 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
return provider.getAllActivitySamples(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
protected List<? extends ActivitySample> getAllSamplesHighRes(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
|
||||
// Only retrieve if the provider signals it has high res data, otherwise it is useless
|
||||
if (provider.hasHighResData())
|
||||
return provider.getAllActivitySamplesHighRes(tsFrom, tsTo);
|
||||
return null;
|
||||
}
|
||||
|
||||
protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends AbstractActivitySample> provider = getProvider(db, device);
|
||||
return provider.getActivitySamples(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
public DefaultChartsData<LineData> refresh(GBDevice gbDevice, List<? extends ActivitySample> samples) {
|
||||
// If there is no high res samples, all the samples are high res samples
|
||||
return refresh(gbDevice, samples, samples);
|
||||
}
|
||||
|
||||
public DefaultChartsData<LineData> refresh(GBDevice gbDevice, List<? extends ActivitySample> samples, List<? extends ActivitySample> highResSamples) {
|
||||
TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
LOG.info("{}: number of samples: {}", getTitle(), samples.size());
|
||||
LOG.info("{}: number of high res samples: {}", getTitle(), highResSamples.size());
|
||||
LineData lineData;
|
||||
|
||||
if (samples.isEmpty()) {
|
||||
@ -257,19 +272,25 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
}
|
||||
entries.get(index).add(createLineEntry(value, ts));
|
||||
|
||||
// heart rate line graph
|
||||
if (hr && type != ActivityKind.NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
|
||||
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
|
||||
heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1));
|
||||
heartrateEntries.add(createLineEntry(0, ts - 1));
|
||||
}
|
||||
heartrateEntries.add(createLineEntry(sample.getHeartRate(), ts));
|
||||
lastHrSampleIndex = ts;
|
||||
}
|
||||
last_type = type;
|
||||
last_value = value;
|
||||
}
|
||||
|
||||
// Currently only for HR
|
||||
if (hr) {
|
||||
for (ActivitySample sample : highResSamples) {
|
||||
if (sample.getKind() != ActivityKind.NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
|
||||
int ts = tsTranslation.shorten(sample.getTimestamp());
|
||||
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
|
||||
heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1));
|
||||
heartrateEntries.add(createLineEntry(0, ts - 1));
|
||||
}
|
||||
heartrateEntries.add(createLineEntry(sample.getHeartRate(), ts));
|
||||
lastHrSampleIndex = ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convert Entry Lists to Datasets
|
||||
List<ILineDataSet> lineDataSets = new ArrayList<>();
|
||||
|
||||
@ -364,15 +385,16 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
|
||||
/**
|
||||
* Implement this to supply the samples to be displayed.
|
||||
*
|
||||
* @param db
|
||||
* @param device
|
||||
* @param tsFrom
|
||||
* @param tsTo
|
||||
* @return
|
||||
*/
|
||||
protected abstract List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo);
|
||||
|
||||
/**
|
||||
* Implement this to supply high resolution data
|
||||
*/
|
||||
protected List<? extends ActivitySample> getSamplesHighRes(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
throw new NotImplementedException("High resolution samples have not been implemented for this chart.");
|
||||
}
|
||||
|
||||
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device) {
|
||||
int tsStart = getTSStart();
|
||||
int tsEnd = getTSEnd();
|
||||
@ -388,6 +410,12 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
return samples;
|
||||
}
|
||||
|
||||
protected List<? extends ActivitySample> getSamplesHighRes(DBHandler db, GBDevice device) {
|
||||
int tsStart = getTSStart();
|
||||
int tsEnd = getTSEnd();
|
||||
return getSamplesHighRes(db, device, tsStart, tsEnd);
|
||||
}
|
||||
|
||||
protected List<? extends ActivitySample> getSamplesofSleep(DBHandler db, GBDevice device) {
|
||||
int SLEEP_HOUR_LIMIT = 12;
|
||||
|
||||
|
@ -207,6 +207,7 @@ public abstract class AbstractChartFragment<D extends ChartsData> extends Abstra
|
||||
protected void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (ChartsHost.REFRESH.equals(action)) {
|
||||
updateDateInfo(getStartDate(), getEndDate());
|
||||
refresh();
|
||||
} else if (ChartsHost.DATE_NEXT_DAY.equals(action)) {
|
||||
handleDate(getStartDate(), getEndDate(), +1);
|
||||
|
@ -129,7 +129,10 @@ public class ActivitySleepChartFragment extends AbstractActivityChartFragment<De
|
||||
@Override
|
||||
protected DefaultChartsData<LineData> refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
List<? extends ActivitySample> samples = getSamples(db, device);
|
||||
return refresh(device, samples);
|
||||
List<? extends ActivitySample> highResSamples = getSamplesHighRes(db, device);
|
||||
if (highResSamples == null)
|
||||
return refresh(device, samples);
|
||||
return refresh(device, samples, highResSamples);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -192,4 +195,9 @@ public class ActivitySleepChartFragment extends AbstractActivityChartFragment<De
|
||||
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
return getAllSamples(db, device, tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<? extends ActivitySample> getSamplesHighRes(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
return getAllSamplesHighRes(db, device, tsFrom, tsTo);
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
@ -92,7 +93,7 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
|
||||
protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends ActivitySample> provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession());
|
||||
return provider.getAllActivitySamples(tsFrom, tsTo);
|
||||
return provider.getAllActivitySamplesHighRes(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -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) {
|
||||
|
@ -60,6 +60,7 @@ public class TemperatureChartFragment extends AbstractChartFragment<TemperatureC
|
||||
|
||||
protected final int TOTAL_DAYS = getRangeDays();
|
||||
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext());
|
||||
|
@ -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) -> {
|
||||
|
@ -126,14 +126,14 @@ public class DashboardHrvWidget extends AbstractGaugeWidget {
|
||||
public static float calculateGaugeValue(int weeklyAverage, int baselineLowUpper, int baselineBalancedLower, int baselineBalancedUpper) {
|
||||
final float value;
|
||||
if (weeklyAverage != 0 && baselineLowUpper != 0 && baselineBalancedLower != 0 && baselineBalancedUpper != 0) {
|
||||
if (weeklyAverage < baselineLowUpper) {
|
||||
value = 0.125f * (float) GaugeDrawer.normalize(weeklyAverage, 0f, baselineLowUpper);
|
||||
if (weeklyAverage <= baselineLowUpper) {
|
||||
value = (float) GaugeDrawer.normalize(weeklyAverage, 0f, baselineLowUpper, 0, 0.124f);
|
||||
} else if (weeklyAverage < baselineBalancedLower) {
|
||||
value = 0.125f + 0.125f * (float) GaugeDrawer.normalize((float) weeklyAverage, baselineLowUpper, baselineBalancedLower);
|
||||
} else if (weeklyAverage < baselineBalancedUpper) {
|
||||
value = 0.125f + 0.125f + 0.5f * (float) GaugeDrawer.normalize((float) weeklyAverage, baselineBalancedLower, baselineBalancedUpper);
|
||||
value = (float) GaugeDrawer.normalize((float) weeklyAverage, baselineLowUpper + 1, baselineBalancedLower - 1, 0.126f, 0.249f);
|
||||
} else if (weeklyAverage <= baselineBalancedUpper) {
|
||||
value = (float) GaugeDrawer.normalize((float) weeklyAverage, baselineBalancedLower, baselineBalancedUpper, 0.251f, 0.749f);
|
||||
} else {
|
||||
value = 0.125f + 0.125f + 0.5f + 0.125f * (float) GaugeDrawer.normalize((float) weeklyAverage, baselineBalancedUpper, 2 * baselineBalancedUpper);
|
||||
value = (float) GaugeDrawer.normalize((float) weeklyAverage, baselineBalancedUpper, 2 * baselineBalancedUpper, 0.751f, 1);
|
||||
}
|
||||
} else {
|
||||
value = -1;
|
||||
|
@ -106,6 +106,12 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_DEVICE_INTERNET_ACCESS = "device_internet_access";
|
||||
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";
|
||||
|
||||
public static final String PREF_BANGLEJS_TEXT_BITMAP = "banglejs_text_bitmap";
|
||||
public static final String PREF_BANGLEJS_TEXT_BITMAP_SIZE = "banglejs_txt_bitmap_size";
|
||||
public static final String PREF_BANGLEJS_WEBVIEW_URL = "banglejs_webview_url";
|
||||
@ -279,6 +285,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";
|
||||
|
@ -70,6 +70,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;
|
||||
@ -611,6 +612,10 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
addPreferenceHandlerFor(PREF_SLEEP_MODE_SLEEP_SCREEN);
|
||||
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);
|
||||
addPreferenceHandlerFor(PREF_HYBRID_HR_FORCE_WHITE_COLOR);
|
||||
addPreferenceHandlerFor(PREF_HYBRID_HR_SAVE_RAW_ACTIVITY_FILES);
|
||||
@ -1047,6 +1052,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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -76,6 +76,17 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
|
||||
return getGBActivitySamples(timestamp_from, timestamp_to);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<T> getAllActivitySamplesHighRes(int timestamp_from, int timestamp_to) {
|
||||
return getGBActivitySamplesHighRes(timestamp_from, timestamp_to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHighResData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@Deprecated // use getAllActivitySamples
|
||||
@ -138,7 +149,7 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the activity samples between two timestamps. Exactly one every minute.
|
||||
* Get the activity samples between two timestamps (inclusive). Exactly one every minute.
|
||||
* @param timestamp_from Start timestamp
|
||||
* @param timestamp_to End timestamp
|
||||
* @return Exactly one sample for every minute
|
||||
@ -162,6 +173,20 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the activity samples between two timestamps (inclusive).
|
||||
* Differs from {@link #getGBActivitySamples(int, int)} in that it supplies as many samples as
|
||||
* available.
|
||||
* It assumes {@link #getGBActivitySamples(int, int)} returns the highest resolution data unless
|
||||
* this is overwritten.
|
||||
* @param timestamp_from Start timestamp
|
||||
* @param timestamp_to End timestamp
|
||||
* @return All the samples between start and end timestamp (inclusive)
|
||||
*/
|
||||
protected List<T> getGBActivitySamplesHighRes(int timestamp_from, int timestamp_to) {
|
||||
return getGBActivitySamples(timestamp_from, timestamp_to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches all samples of this type from the session. Changes to them may not be
|
||||
* written back to the database.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ public interface SampleProvider<T extends AbstractActivitySample> {
|
||||
|
||||
/**
|
||||
* Returns the list of all samples, of any type, within the given time span.
|
||||
* This returns exactly one sample every minute.
|
||||
* @param timestamp_from the start timestamp
|
||||
* @param timestamp_to the end timestamp
|
||||
* @return the list of samples of any type
|
||||
@ -54,6 +55,19 @@ public interface SampleProvider<T extends AbstractActivitySample> {
|
||||
@NonNull
|
||||
List<T> getAllActivitySamples(int timestamp_from, int timestamp_to);
|
||||
|
||||
/**
|
||||
* Same as {@link #getAllActivitySamples(int, int)}}, but returns as many samples as possible.
|
||||
* Explicitly does not make a guarantee about how many samples there are per timeframe, which
|
||||
* can also change over time.
|
||||
*/
|
||||
List<T> getAllActivitySamplesHighRes(int timestamp_from, int timestamp_to);
|
||||
|
||||
/**
|
||||
* Specifies that the sample provider has higher resolution data. Set to true if the sample
|
||||
* provider can provide more than one sample a minute.
|
||||
*/
|
||||
boolean hasHighResData();
|
||||
|
||||
/**
|
||||
* Returns the list of all samples that represent user "activity", within
|
||||
* the given time span. This excludes samples of type sleep, for example.
|
||||
|
@ -21,6 +21,7 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
@ -64,6 +65,16 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractActivitySample> getAllActivitySamplesHighRes(int timestamp_from, int timestamp_to) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHighResData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractActivitySample> getActivitySamples(int timestamp_from, int timestamp_to) {
|
||||
return null;
|
||||
|
@ -0,0 +1,63 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.bandwpseries;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
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.bandwpseries.BandWPSeriesDeviceSupport;
|
||||
|
||||
public class BandWPSeriesDeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_bandw_pseries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Bowers and Wilkins";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return BandWPSeriesDeviceSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("LE_BWHP");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatteryCount() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public BatteryConfig[] getBatteryConfig(final GBDevice device) {
|
||||
BatteryConfig battery0 = new BatteryConfig(0, R.drawable.ic_earbuds_battery, R.string.left_earbud);
|
||||
BatteryConfig battery1 = new BatteryConfig(1, R.drawable.ic_earbuds_battery, R.string.right_earbud);
|
||||
BatteryConfig battery2 = new BatteryConfig(2, R.drawable.ic_tws_case, R.string.battery_case);
|
||||
return new BatteryConfig[]{battery0, battery1, battery2};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[] {
|
||||
R.xml.devicesettings_active_noise_cancelling_toggle,
|
||||
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,18 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminFenix6SProCoordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^fenix 6S Pro$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_fenix_6s_pro;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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 GarminForerunner235Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^Forerunner 235$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_forerunner_235;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ public class HeartRateZonesConfig {
|
||||
public static final int MAXIMUM_HEART_RATE = 220;
|
||||
|
||||
private final int configType;
|
||||
private int calculateMethod = 0;
|
||||
private int calculateMethod = 0; // 0 - MHR, 1 - HRR, 3 - LTHR
|
||||
|
||||
private int maxHRThreshold;
|
||||
private int restHeartRate = DEFAULT_REST_HEART_RATE;
|
||||
@ -231,4 +231,35 @@ public class HeartRateZonesConfig {
|
||||
return LTHRThresholdHeartRate > 0 && LTHRAnaerobic > 0 && LTHRLactate > 0 && LTHRAdvancedAerobic > 0 && LTHRBasicAerobic > 0 && LTHRWarmUp > 0;
|
||||
}
|
||||
|
||||
private int getZoneForHR(int heartRate, int zone5Threshold, int zone4Threshold, int zone3Threshold, int zone2Threshold, int zone1Threshold) {
|
||||
if (heartRate >= MAXIMUM_HEART_RATE) {
|
||||
return -1;
|
||||
}
|
||||
if (heartRate >= zone5Threshold) {
|
||||
return 4;
|
||||
}
|
||||
if (heartRate >= zone4Threshold) {
|
||||
return 3;
|
||||
}
|
||||
if (heartRate >= zone3Threshold) {
|
||||
return 2;
|
||||
}
|
||||
if (heartRate >= zone2Threshold) {
|
||||
return 1;
|
||||
}
|
||||
return heartRate >= zone1Threshold ? 0 : -1;
|
||||
}
|
||||
|
||||
public int getMHRZone(int heartRate) {
|
||||
return getZoneForHR(heartRate, MHRExtreme, MHRAnaerobic, MHRAerobic, MHRFatBurning, MHRWarmUp);
|
||||
}
|
||||
|
||||
public int getHHRZone(int heartRate) {
|
||||
return getZoneForHR(heartRate, HRRAdvancedAnaerobic, HRRBasicAnaerobic, HRRLactate, HRRAdvancedAerobic, HRRBasicAerobic);
|
||||
}
|
||||
|
||||
public int getLTHRZone(int heartRate) {
|
||||
return getZoneForHR(heartRate, LTHRAnaerobic, LTHRLactate, LTHRAdvancedAerobic, LTHRBasicAerobic, LTHRWarmUp);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiBRSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
|
||||
@ -187,6 +188,11 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
|
||||
return huaweiCoordinator.supportsMusic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTemperatureMeasurement() {
|
||||
return huaweiCoordinator.supportsTemperature();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return huaweiCoordinator.getInstallHandler(uri, context);
|
||||
@ -207,6 +213,11 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends TemperatureSample> getTemperatureSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
return new HuaweiTemperatureSampleProvider(device, session);
|
||||
}
|
||||
|
||||
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||
return huaweiCoordinator.getDeviceSpecificSettings(device);
|
||||
}
|
||||
|
@ -56,7 +56,9 @@ public final class HuaweiConstants {
|
||||
public static final String HU_BAND4E_NAME = "huawei band 4e-";
|
||||
public static final String HU_BAND6_NAME = "huawei band 6-";
|
||||
public static final String HU_WATCHGT_NAME = "huawei watch gt-";
|
||||
public static final String HU_BAND3_NAME = "huawei band 3-";
|
||||
public static final String HU_BAND4_NAME = "huawei band 4-";
|
||||
public static final String HU_BAND3PRO_NAME = "huawei band 3 pro-";
|
||||
public static final String HU_BAND4PRO_NAME = "huawei band 4 pro-";
|
||||
public static final String HU_WATCHGT2_NAME = "huawei watch gt 2-";
|
||||
public static final String HU_WATCHGT2E_NAME = "huawei watch gt 2e-";
|
||||
|
@ -46,6 +46,9 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataValuesDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
|
||||
@ -134,6 +137,16 @@ public class HuaweiCoordinator {
|
||||
session.getHuaweiWorkoutSummarySampleDao().queryBuilder().where(HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
session.getBaseActivitySummaryDao().queryBuilder().where(BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
QueryBuilder<HuaweiDictData> qb3 = session.getHuaweiDictDataDao().queryBuilder();
|
||||
List<HuaweiDictData> dictData = qb3.where(HuaweiDictDataDao.Properties.DeviceId.eq(deviceId)).build().list();
|
||||
for (HuaweiDictData data : dictData) {
|
||||
session.getHuaweiDictDataValuesDao().queryBuilder().where(
|
||||
HuaweiDictDataValuesDao.Properties.DictId.eq(data.getDictId())
|
||||
).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
}
|
||||
|
||||
session.getHuaweiDictDataDao().queryBuilder().where(HuaweiDictDataDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
}
|
||||
|
||||
private SharedPreferences getCapabilitiesSharedPreferences() {
|
||||
@ -292,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);
|
||||
@ -435,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);
|
||||
}
|
||||
@ -589,8 +615,6 @@ public class HuaweiCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean supportsCalendar() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(171) || supportsExpandCapability(184);
|
||||
@ -615,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
|
||||
|
@ -42,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiLESupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
|
||||
@ -196,6 +197,11 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
|
||||
return huaweiCoordinator.supportsMusic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTemperatureMeasurement() {
|
||||
return huaweiCoordinator.supportsTemperature();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return huaweiCoordinator.getInstallHandler(uri, context);
|
||||
@ -216,6 +222,11 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
|
||||
return new HuaweiSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends TemperatureSample> getTemperatureSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
return new HuaweiTemperatureSampleProvider(device, session);
|
||||
}
|
||||
|
||||
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||
return huaweiCoordinator.getDeviceSpecificSettings(device);
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
@ -305,12 +306,25 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
|
||||
return processedSamples;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<HuaweiActivitySample> getGBActivitySamplesHighRes(int timestamp_from, int timestamp_to) {
|
||||
List<HuaweiActivitySample> processedSamples = getRawOrderedActivitySamples(timestamp_from, timestamp_to);
|
||||
addWorkoutSamples(processedSamples, timestamp_from, timestamp_to);
|
||||
// Filter out the end markers before returning
|
||||
return processedSamples.stream().filter(sample -> sample.getTimestamp() <= sample.getOtherTimestamp()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHighResData() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private HuaweiActivitySample createDummySample(int timestamp) {
|
||||
HuaweiActivitySample activitySample = new HuaweiActivitySample(
|
||||
timestamp,
|
||||
-1,
|
||||
-1,
|
||||
0,
|
||||
timestamp + 60, // Make sure the duration is 60
|
||||
(byte) 0x00,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
0,
|
||||
@ -345,6 +359,10 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
|
||||
int stateModifier = ActivitySample.NOT_MEASURED;
|
||||
|
||||
for (HuaweiActivitySample activitySample : activitySamples) {
|
||||
// Ignore the end markers
|
||||
if (activitySample.getTimestamp() > activitySample.getOtherTimestamp())
|
||||
continue;
|
||||
|
||||
// Skip the processed samples that are before this activity sample
|
||||
while (activitySample.getTimestamp() > processedSamples.get(currentIndex).getTimestamp()) {
|
||||
// Add data to current index sample
|
||||
@ -438,7 +456,7 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
|
||||
List<HuaweiWorkoutDataSample> workoutSamples = getRawOrderedWorkoutSamplesWithHeartRate(timestamp_from, timestamp_to);
|
||||
|
||||
for (int i = 0; i < workoutSamples.size(); i++) {
|
||||
// Look ahead to see if this is still the same workout
|
||||
// Look behind to see if this is still the same workout
|
||||
boolean inWorkout = i != 0 && workoutSamples.get(i).getWorkoutId() == workoutSamples.get(i - 1).getWorkoutId();
|
||||
|
||||
// Skip the processed sample that are before this workout sample
|
||||
@ -470,4 +488,53 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
|
||||
processedSamples.get(currentIndex).setRawIntensity(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void addWorkoutSamples(List<HuaweiActivitySample> processedSamples, int timestamp_from, int timestamp_to) {
|
||||
int currentIndex = 0;
|
||||
List<HuaweiWorkoutDataSample> workoutSamples = getRawOrderedWorkoutSamplesWithHeartRate(timestamp_from, timestamp_to);
|
||||
|
||||
for (int i = 0; i < workoutSamples.size(); i++) {
|
||||
// Look behind to see if this is still the same workout
|
||||
boolean inWorkout = i != 0 && workoutSamples.get(i).getWorkoutId() == workoutSamples.get(i - 1).getWorkoutId();
|
||||
|
||||
// Skip the samples that are before this workout sample, and potentially clear the HR
|
||||
// and intensity - see #4126 for the reasoning
|
||||
while (currentIndex < processedSamples.size() && workoutSamples.get(i).getTimestamp() > processedSamples.get(currentIndex).getTimestamp()) {
|
||||
if (inWorkout) {
|
||||
processedSamples.get(currentIndex).setHeartRate(ActivitySample.NOT_MEASURED);
|
||||
processedSamples.get(currentIndex).setRawIntensity(0);
|
||||
}
|
||||
|
||||
currentIndex += 1;
|
||||
}
|
||||
|
||||
if (i < workoutSamples.size() - 1) {
|
||||
processedSamples.add(currentIndex, convertWorkoutSampleToActivitySample(workoutSamples.get(i), workoutSamples.get(i + 1).getTimestamp()));
|
||||
} else {
|
||||
// For the last workout sample we assume it is over 5 seconds
|
||||
processedSamples.add(currentIndex, convertWorkoutSampleToActivitySample(workoutSamples.get(i), workoutSamples.get(i).getTimestamp() + 5));
|
||||
}
|
||||
currentIndex += 1; // Prevent clearing the sample in the next loop
|
||||
}
|
||||
}
|
||||
|
||||
private HuaweiActivitySample convertWorkoutSampleToActivitySample(HuaweiWorkoutDataSample workoutSample, int nextTimestamp) {
|
||||
int hr = workoutSample.getHeartRate() & 0xFF;
|
||||
HuaweiActivitySample newSample = new HuaweiActivitySample(
|
||||
workoutSample.getTimestamp(),
|
||||
-1,
|
||||
-1,
|
||||
nextTimestamp - 1, // Just to prevent overlap causing issues
|
||||
(byte) 0x00,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
hr
|
||||
);
|
||||
newSample.setProvider(this);
|
||||
return newSample;
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,8 @@ public class HuaweiSpo2SampleProvider extends AbstractTimeSampleProvider<HuaweiS
|
||||
@NonNull
|
||||
@Override
|
||||
public List<HuaweiSpo2Sample> getAllSamples(long timestampFrom, long timestampTo) {
|
||||
List<HuaweiActivitySample> activitySamples = huaweiSampleProvider.getAllActivitySamples((int) (timestampFrom / 1000L), (int) (timestampTo / 1000L));
|
||||
// Using high res data is fine for the SpO2 sample provider at the time of writing
|
||||
List<HuaweiActivitySample> activitySamples = huaweiSampleProvider.getAllActivitySamplesHighRes((int) (timestampFrom / 1000L), (int) (timestampTo / 1000L));
|
||||
List<HuaweiSpo2Sample> spo2Samples = new ArrayList<>(activitySamples.size());
|
||||
for (HuaweiActivitySample sample : activitySamples) {
|
||||
if (sample.getSpo() == -1)
|
||||
|
@ -223,10 +223,26 @@ public class HuaweiTLV {
|
||||
return getBytes(tag)[0];
|
||||
}
|
||||
|
||||
public Byte getByte(int tag, Byte defaultValue) {
|
||||
try {
|
||||
return getByte(tag);
|
||||
} catch (HuaweiPacket.MissingTagException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean getBoolean(int tag) throws HuaweiPacket.MissingTagException {
|
||||
return getBytes(tag)[0] == 1;
|
||||
}
|
||||
|
||||
public Boolean getBoolean(int tag, Boolean defaultValue) {
|
||||
try {
|
||||
return getBoolean(tag);
|
||||
} catch (HuaweiPacket.MissingTagException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public Integer getInteger(int tag) throws HuaweiPacket.MissingTagException {
|
||||
return ByteBuffer.wrap(getBytes(tag)).getInt();
|
||||
}
|
||||
@ -243,6 +259,18 @@ public class HuaweiTLV {
|
||||
return ByteBuffer.wrap(getBytes(tag)).getShort();
|
||||
}
|
||||
|
||||
public Short getShort(int tag, Short defaultValue) {
|
||||
try {
|
||||
return getShort(tag);
|
||||
} catch (HuaweiPacket.MissingTagException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public Long getLong(int tag) throws HuaweiPacket.MissingTagException {
|
||||
return ByteBuffer.wrap(getBytes(tag)).getLong();
|
||||
}
|
||||
|
||||
public Integer getAsInteger(int tag) throws HuaweiPacket.MissingTagException {
|
||||
byte[] bytes = getBytes(tag);
|
||||
if(bytes.length == 1) {
|
||||
|
@ -0,0 +1,190 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.greenrobot.dao.Property;
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataValues;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataValuesDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PDataDictionarySyncService;
|
||||
|
||||
public class HuaweiTemperatureSampleProvider implements TimeSampleProvider<TemperatureSample> {
|
||||
|
||||
private final Logger LOG = LoggerFactory.getLogger(HuaweiTemperatureSampleProvider.class);
|
||||
|
||||
protected static class HuaweiTemperatureSample implements TemperatureSample {
|
||||
private final long timestamp;
|
||||
private final float temperature;
|
||||
|
||||
public HuaweiTemperatureSample(long timestamp, float temperature) {
|
||||
this.timestamp = timestamp;
|
||||
this.temperature = temperature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTemperature() {
|
||||
return temperature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTemperatureType() { return 0;}
|
||||
}
|
||||
|
||||
private final GBDevice device;
|
||||
private final DaoSession session;
|
||||
|
||||
public HuaweiTemperatureSampleProvider(GBDevice device, DaoSession session) {
|
||||
this.device = device;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
private double conv2Double(byte[] b) {
|
||||
return ByteBuffer.wrap(b).getDouble();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<TemperatureSample> getAllSamples(long timestampFrom, long timestampTo) {
|
||||
|
||||
List<TemperatureSample> ret = new ArrayList<>();
|
||||
|
||||
Long userId = DBHelper.getUser(this.session).getId();
|
||||
Long deviceId = DBHelper.getDevice(this.device, this.session).getId();
|
||||
|
||||
if (deviceId == null || userId == null)
|
||||
return ret;
|
||||
|
||||
QueryBuilder<HuaweiDictData> qb = this.session.getHuaweiDictDataDao().queryBuilder();
|
||||
qb.where(HuaweiDictDataDao.Properties.DeviceId.eq(deviceId))
|
||||
.where(HuaweiDictDataDao.Properties.UserId.eq(userId))
|
||||
.where(HuaweiDictDataDao.Properties.DictClass.eq(400012))
|
||||
.where(HuaweiDictDataDao.Properties.StartTimestamp.between(timestampFrom, timestampTo));
|
||||
final List<HuaweiDictData> dictData = qb.build().list();
|
||||
|
||||
if (dictData.isEmpty())
|
||||
return ret;
|
||||
|
||||
List<Long> ids = dictData.stream().map(HuaweiDictData::getDictId).collect(Collectors.toList());
|
||||
|
||||
QueryBuilder<HuaweiDictDataValues> qbv = this.session.getHuaweiDictDataValuesDao().queryBuilder();
|
||||
|
||||
qbv.where(HuaweiDictDataValuesDao.Properties.DictType.eq(400012430)).where(HuaweiDictDataValuesDao.Properties.Tag.eq(10)).where(HuaweiDictDataValuesDao.Properties.DictId.in(ids));
|
||||
|
||||
final List<HuaweiDictDataValues> valuesData = qbv.build().list();
|
||||
|
||||
if (valuesData.isEmpty())
|
||||
return ret;
|
||||
|
||||
for(HuaweiDictDataValues vl: valuesData) {
|
||||
double skinTemperature = conv2Double(vl.getValue());
|
||||
if(skinTemperature >= 20 && skinTemperature <= 42) {
|
||||
ret.add(new HuaweiTemperatureSample(vl.getHuaweiDictData().getStartTimestamp(), (float) skinTemperature));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSample(TemperatureSample timeSample) {
|
||||
throw new UnsupportedOperationException("read-only sample provider");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSamples(List<TemperatureSample> timeSamples) {
|
||||
throw new UnsupportedOperationException("read-only sample provider");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public TemperatureSample createSample() {
|
||||
throw new UnsupportedOperationException("read-only sample provider");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public TemperatureSample getLatestSample() {
|
||||
Long userId = DBHelper.getUser(this.session).getId();
|
||||
Long deviceId = DBHelper.getDevice(this.device, this.session).getId();
|
||||
|
||||
if (deviceId == null || userId == null)
|
||||
return null;
|
||||
|
||||
QueryBuilder<HuaweiDictData> qb = this.session.getHuaweiDictDataDao().queryBuilder();
|
||||
qb.where(HuaweiDictDataDao.Properties.DeviceId.eq(deviceId))
|
||||
.where(HuaweiDictDataDao.Properties.UserId.eq(userId))
|
||||
.where(HuaweiDictDataDao.Properties.DictClass.eq(400012));
|
||||
qb.orderDesc(HuaweiDictDataDao.Properties.StartTimestamp).limit(1);
|
||||
|
||||
final List<HuaweiDictData> data = qb.build().list();
|
||||
if (data.isEmpty())
|
||||
return null;
|
||||
|
||||
|
||||
QueryBuilder<HuaweiDictDataValues> qbv = this.session.getHuaweiDictDataValuesDao().queryBuilder();
|
||||
qbv.where(HuaweiDictDataValuesDao.Properties.DictType.eq(400012430)).where(HuaweiDictDataValuesDao.Properties.Tag.eq(10)).where(HuaweiDictDataValuesDao.Properties.DictId.eq(data.get(0).getDictId()));
|
||||
final List<HuaweiDictDataValues> valuesData = qbv.build().list();
|
||||
|
||||
if (valuesData.isEmpty())
|
||||
return null;
|
||||
|
||||
return new HuaweiTemperatureSample(valuesData.get(0).getHuaweiDictData().getStartTimestamp(), (float) conv2Double(valuesData.get(0).getValue()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public TemperatureSample getFirstSample() {
|
||||
Long userId = DBHelper.getUser(this.session).getId();
|
||||
Long deviceId = DBHelper.getDevice(this.device, this.session).getId();
|
||||
|
||||
if (deviceId == null || userId == null)
|
||||
return null;
|
||||
|
||||
QueryBuilder<HuaweiDictData> qb = this.session.getHuaweiDictDataDao().queryBuilder();
|
||||
qb.where(HuaweiDictDataDao.Properties.DeviceId.eq(deviceId))
|
||||
.where(HuaweiDictDataDao.Properties.UserId.eq(userId))
|
||||
.where(HuaweiDictDataDao.Properties.DictClass.eq(400012));
|
||||
qb.orderAsc(HuaweiDictDataDao.Properties.StartTimestamp).limit(1);
|
||||
|
||||
final List<HuaweiDictData> data = qb.build().list();
|
||||
if (data.isEmpty())
|
||||
return null;
|
||||
|
||||
QueryBuilder<HuaweiDictDataValues> qbv = this.session.getHuaweiDictDataValuesDao().queryBuilder();
|
||||
qbv.where(HuaweiDictDataValuesDao.Properties.DictType.eq(400012430)).where(HuaweiDictDataValuesDao.Properties.Tag.eq(10)).where(HuaweiDictDataValuesDao.Properties.DictId.eq(data.get(0).getDictId()));
|
||||
final List<HuaweiDictDataValues> valuesData = qbv.build().list();
|
||||
|
||||
if (valuesData.isEmpty())
|
||||
return null;
|
||||
|
||||
return new HuaweiTemperatureSample(valuesData.get(0).getHuaweiDictData().getStartTimestamp(), (float) conv2Double(valuesData.get(0).getValue()));
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/* Copyright (C) 2024 Damien Gaignon, Guido Jäkel, 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.devices.huawei.huaweiband3pro;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class HuaweiBand3ProCoordinator extends HuaweiLECoordinator {
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIBAND3PRO;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("(" + HuaweiConstants.HU_BAND3_NAME + "|" + HuaweiConstants.HU_BAND3PRO_NAME + ").*", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_band3pro;
|
||||
}
|
||||
}
|
@ -92,11 +92,11 @@ public class Alarms {
|
||||
|
||||
public SmartAlarm(HuaweiTLV tlv) throws ParseException {
|
||||
this.index = tlv.getByte(0x03);
|
||||
this.status = tlv.getBoolean(0x04);
|
||||
this.startHour = (byte) ((tlv.getShort(0x05) >> 8) & 0xFF);
|
||||
this.startMinute = (byte) (tlv.getShort(0x05) & 0xFF);
|
||||
this.repeat = tlv.getByte(0x06);
|
||||
this.aheadTime = tlv.getByte(0x07);
|
||||
this.status = tlv.getBoolean(0x04, false);
|
||||
this.startHour = (byte) ((tlv.getShort(0x05, (short) 0) >> 8) & 0xFF);
|
||||
this.startMinute = (byte) (tlv.getShort(0x05, (short) 0) & 0xFF);
|
||||
this.repeat = tlv.getByte(0x06, (byte) 0);
|
||||
this.aheadTime = tlv.getByte(0x07, (byte) 0);
|
||||
}
|
||||
|
||||
public SmartAlarm(boolean status, byte startHour, byte startMinute, byte repeat, byte aheadTime) {
|
||||
|
@ -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,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
|
||||
));
|
||||
}};
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@ public class SonyWFC500Coordinator extends SonyHeadphonesCoordinator {
|
||||
SonyHeadphonesCapabilities.EqualizerSimple,
|
||||
SonyHeadphonesCapabilities.EqualizerWithCustomBands,
|
||||
SonyHeadphonesCapabilities.AudioUpsampling,
|
||||
SonyHeadphonesCapabilities.VoiceNotifications,
|
||||
SonyHeadphonesCapabilities.PowerOffFromPhone
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
/* Copyright (C) 2024 Zahnstocher
|
||||
|
||||
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.sony.headphones.coordinators;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCapabilities;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator;
|
||||
|
||||
public class SonyWIC100Coordinator extends SonyHeadphonesCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile(".*WI-C100.*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SonyHeadphonesCapabilities> getCapabilities() {
|
||||
return Arrays.asList(
|
||||
SonyHeadphonesCapabilities.BatterySingle,
|
||||
SonyHeadphonesCapabilities.EqualizerSimple,
|
||||
SonyHeadphonesCapabilities.EqualizerWithCustomBands,
|
||||
SonyHeadphonesCapabilities.AudioUpsampling,
|
||||
SonyHeadphonesCapabilities.VoiceNotifications,
|
||||
SonyHeadphonesCapabilities.PowerOffFromPhone
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_sony_wi_c100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_galaxy_buds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDisabledIconResource() {
|
||||
return R.drawable.ic_device_galaxy_buds_disabled;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
@ -2,7 +2,7 @@
|
||||
Andreas Böhler, Andreas Shimokawa, Andrew Watkins, angelpup, Carsten Pfeiffer,
|
||||
Cre3per, Damien Gaignon, DanialHanif, Daniel Dakhno, Daniele Gobbetti, Daniel
|
||||
Thompson, Da Pa, Dmytro Bielik, Frank Ertl, Gabriele Monaco, GeekosaurusR3x,
|
||||
Gordon Williams, Jean-François Greffier, jfgreffier, jhey, João Paulo
|
||||
Guido Jäkel, Gordon Williams, Jean-François Greffier, jfgreffier, jhey, João Paulo
|
||||
Barraca, Jochen S, Johannes Krude, José Rebelo, ksiwczynski, ladbsoft,
|
||||
Lesur Frederic, Maciej Kuśnierz, mamucho, Manuel Ruß, Maxime Reyrolle,
|
||||
maxirnilian, Michael, narektor, Noodlez, odavo32nof, opavlov, pangwalla,
|
||||
@ -30,6 +30,7 @@ package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.asteroidos.AsteroidOSDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.bandwpseries.BandWPSeriesDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.binary_sensor.coordinator.BinarySensorCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator;
|
||||
@ -61,6 +62,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminF
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix5PlusCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix5XPlusCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix6Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix6SProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix6SSapphireCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix6SapphireCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix7Coordinator;
|
||||
@ -68,6 +70,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminF
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix7SCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix8Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner165Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner235Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner245Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner245MusicCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner255Coordinator;
|
||||
@ -76,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;
|
||||
@ -172,6 +178,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorband7.HonorBand7
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honormagicwatch2.HonorMagicWatch2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorwatchgs3.HonorWatchGS3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorwatchgspro.HonorWatchGSProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband3pro.HuaweiBand3ProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband4pro.HuaweiBand4ProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband6.HuaweiBand6Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband7.HuaweiBand7Coordinator;
|
||||
@ -220,10 +227,12 @@ 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;
|
||||
@ -241,6 +250,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM4Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM5Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWIC100Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWISP600NCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
|
||||
@ -418,12 +428,15 @@ public enum DeviceType {
|
||||
GARMIN_FENIX_5X_PLUS(GarminFenix5XPlusCoordinator.class),
|
||||
GARMIN_FENIX_6(GarminFenix6Coordinator.class),
|
||||
GARMIN_FENIX_6_SAPPHIRE(GarminFenix6SapphireCoordinator.class),
|
||||
GARMIN_FENIX_6S_PRO(GarminFenix6SProCoordinator.class),
|
||||
GARMIN_FENIX_6S_SAPPHIRE(GarminFenix6SSapphireCoordinator.class),
|
||||
GARMIN_FENIX_7(GarminFenix7Coordinator.class),
|
||||
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),
|
||||
GARMIN_FORERUNNER_245_MUSIC(GarminForerunner245MusicCoordinator.class),
|
||||
GARMIN_FORERUNNER_255(GarminForerunner255Coordinator.class),
|
||||
@ -432,11 +445,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),
|
||||
@ -478,6 +493,7 @@ public enum DeviceType {
|
||||
GALAXY_BUDS2_PRO(GalaxyBuds2ProDeviceCoordinator.class),
|
||||
SONY_WH_1000XM3(SonyWH1000XM3Coordinator.class),
|
||||
SONY_WF_SP800N(SonyWFSP800NCoordinator.class),
|
||||
SONY_WI_C100(SonyWIC100Coordinator.class),
|
||||
SONY_WI_SP600N(SonyWISP600NCoordinator.class),
|
||||
SONY_WH_1000XM4(SonyWH1000XM4Coordinator.class),
|
||||
SONY_WF_1000XM3(SonyWF1000XM3Coordinator.class),
|
||||
@ -500,6 +516,7 @@ public enum DeviceType {
|
||||
HUAWEIBANDAW70(HuaweiBandAw70Coordinator.class),
|
||||
HUAWEIBAND6(HuaweiBand6Coordinator.class),
|
||||
HUAWEIWATCHGT(HuaweiWatchGTCoordinator.class),
|
||||
HUAWEIBAND3PRO(HuaweiBand3ProCoordinator.class),
|
||||
HUAWEIBAND4PRO(HuaweiBand4ProCoordinator.class),
|
||||
HUAWEIWATCHGT2(HuaweiWatchGT2Coordinator.class),
|
||||
HUAWEIWATCHGT2E(HuaweiWatchGT2eCoordinator.class),
|
||||
@ -529,6 +546,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),
|
||||
@ -539,6 +558,7 @@ public enum DeviceType {
|
||||
COLMI_R03(ColmiR03Coordinator.class),
|
||||
COLMI_R06(ColmiR06Coordinator.class),
|
||||
COLMI_R10(ColmiR10Coordinator.class),
|
||||
B_AND_W_P_SERIES(BandWPSeriesDeviceCoordinator.class),
|
||||
SCANNABLE(ScannableDeviceCoordinator.class),
|
||||
CYCLING_SENSOR(CyclingSensorCoordinator.class),
|
||||
BLE_GATT_CLIENT(BleGattClientCoordinator.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);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,88 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
|
||||
|
||||
public class BandWBLEProfile<T extends AbstractBTLEDeviceSupport> extends AbstractBleProfile<T> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BandWBLEProfile.class);
|
||||
|
||||
private static final String ACTION_PREFIX = BandWBLEProfile.class.getName() + "_";
|
||||
|
||||
public static final String ACTION_DEVICE_INFO = ACTION_PREFIX + "DEVICE_INFO";
|
||||
public static final String EXTRA_DEVICE_INFO = "DEVICE_INFO";
|
||||
|
||||
public static final byte ANC_MODE_OFF = 0x01;
|
||||
public static final byte ANC_MODE_ON = 0x03;
|
||||
|
||||
public static final UUID UUID_RPC_REQUEST_CHARACTERISTIC = UUID.fromString("ada50ce9-67b8-4a97-9d8e-37e1d083156c");
|
||||
|
||||
public BandWBLEProfile(final T support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
public void requestAncModeState(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x03, (byte) 0x01);
|
||||
}
|
||||
public void requestDeviceName(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x05, (byte) 0x01);
|
||||
}
|
||||
|
||||
public void requestFirmware(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x02, (byte) 0x01);
|
||||
}
|
||||
|
||||
public void requestBatteryLevels(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x08, (byte) 0x17);
|
||||
}
|
||||
|
||||
public void requestVptEnabled(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x03, (byte) 0x05);
|
||||
}
|
||||
|
||||
public void requestVptLevel(final TransactionBuilder builder) {
|
||||
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());
|
||||
}
|
||||
|
||||
public void setVptLevel(final TransactionBuilder builder, final int level) throws IOException {
|
||||
BandWPSeriesRequest req = new BandWPSeriesRequest((byte) 0x03, (byte) 0x04).addToPayload(level);
|
||||
builder.write(getCharacteristic(UUID_RPC_REQUEST_CHARACTERISTIC), req.finishAndGetBytes());
|
||||
}
|
||||
|
||||
public void setVptEnabled(final TransactionBuilder builder, final boolean mode) throws IOException {
|
||||
BandWPSeriesRequest req = new BandWPSeriesRequest((byte) 0x03, (byte) 0x06).addToPayload(mode);
|
||||
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 {
|
||||
req = new BandWPSeriesRequest(namespace, commandID);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to send request: namespace {}, commandID {}", namespace, commandID);
|
||||
return;
|
||||
}
|
||||
builder.write(getCharacteristic(UUID_RPC_REQUEST_CHARACTERISTIC), req.finishAndGetBytes());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
|
||||
public enum BandWMessageType {
|
||||
REQUEST_WITH_PAYLOAD(0x920b, true),
|
||||
REQUEST_WITHOUT_PAYLOAD(0x120b, false),
|
||||
RESPONSE_WITH_PAYLOAD(0x920c, true),
|
||||
RESPONSE_WITHOUT_PAYLOAD(0x120c, false),
|
||||
NOTIFICATION_WITH_PAYLOAD(0x920d, true),
|
||||
NOTIFICATION_WITHOUT_PAYLOAD(0x120d, false);
|
||||
|
||||
|
||||
public final int value;
|
||||
public final boolean hasPayload;
|
||||
|
||||
BandWMessageType(int mType, boolean hasPayload) {
|
||||
this.value = mType;
|
||||
this.hasPayload = hasPayload;
|
||||
}
|
||||
|
||||
public static BandWMessageType getByType(int mType) {
|
||||
for (BandWMessageType t: values()) {
|
||||
if (t.value == mType) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,315 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ACTIVE_NOISE_CANCELLING_TOGGLE;
|
||||
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;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.BFH16Constants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BandWPSeriesDeviceSupport.class);
|
||||
|
||||
private static final UUID UUID_RPC_SERVICE = UUID.fromString("85ba93a5-09ac-439a-8cc4-1c3f0cb4f29f");
|
||||
private static final UUID UUID_RPC_RESPONSE_CHARACTERISTIC = UUID.fromString("cb909093-3559-4b0c-9a7f-3f1773122fdc");
|
||||
private static final UUID UUID_RPC_NOTIFICATION_CHARACTERISTIC = UUID.fromString("df55d475-9a32-457a-9e20-38cf14e853fb");
|
||||
|
||||
private final BandWBLEProfile<BandWPSeriesDeviceSupport> BandWBLEProfile;
|
||||
private final GBDeviceEventBatteryInfo[] batteryInfo = new GBDeviceEventBatteryInfo[3];
|
||||
|
||||
public BandWPSeriesDeviceSupport() {
|
||||
super(LOG);
|
||||
addSupportedService(BFH16Constants.BFH16_GENERIC_ATTRIBUTE_SERVICE);
|
||||
addSupportedService(BFH16Constants.BFH16_GENERIC_ACCESS_SERVICE);
|
||||
addSupportedService(UUID_RPC_SERVICE);
|
||||
|
||||
BandWBLEProfile = new BandWBLEProfile<>(this);
|
||||
addSupportedProfile(BandWBLEProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
// mark the device as initializing
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
|
||||
getDevice().setBatteryLabel(R.string.left_earbud, 0);
|
||||
getDevice().setBatteryLabel(R.string.right_earbud, 1);
|
||||
getDevice().setBatteryLabel(R.string.battery_case, 2);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
batteryInfo[i] = new GBDeviceEventBatteryInfo();
|
||||
batteryInfo[i].batteryIndex = i;
|
||||
batteryInfo[i].level = BATTERY_UNKNOWN;
|
||||
handleGBDeviceEvent(batteryInfo[i]);
|
||||
}
|
||||
|
||||
// mark the device as initialized
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||
|
||||
builder.notify(getCharacteristic(UUID_RPC_RESPONSE_CHARACTERISTIC), true);
|
||||
builder.notify(getCharacteristic(UUID_RPC_NOTIFICATION_CHARACTERISTIC), true);
|
||||
BandWBLEProfile.requestFirmware(builder);
|
||||
BandWBLEProfile.requestDeviceName(builder);
|
||||
BandWBLEProfile.requestBatteryLevels(builder);
|
||||
BandWBLEProfile.requestAncModeState(builder);
|
||||
BandWBLEProfile.requestVptEnabled(builder);
|
||||
BandWBLEProfile.requestVptLevel(builder);
|
||||
BandWBLEProfile.requestWearSensorEnabled(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
super.onCharacteristicChanged(gatt, characteristic);
|
||||
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
|
||||
if (UUID_RPC_RESPONSE_CHARACTERISTIC.equals(characteristicUUID) || UUID_RPC_NOTIFICATION_CHARACTERISTIC.equals(characteristicUUID)) {
|
||||
return handleRPCResponse(characteristic);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean handleRPCResponse(BluetoothGattCharacteristic characteristic) {
|
||||
BandWPSeriesResponse response = new BandWPSeriesResponse(characteristic.getValue());
|
||||
LOG.debug("Got RPC response: Type {}, commandID {}, namespace {}, errorCode {}, payload {}",
|
||||
response.messageType,
|
||||
response.commandId,
|
||||
response.namespace,
|
||||
response.errorCode,
|
||||
response.payload);
|
||||
if (response.errorCode != 0) {
|
||||
return false;
|
||||
}
|
||||
if (response.namespace == 0x02) {
|
||||
if (response.commandId == 0x01) {
|
||||
return handleFirmwareVersionResponse(response);
|
||||
}
|
||||
} else if (response.namespace == 0x03) {
|
||||
switch (response.commandId) {
|
||||
case 0x01:
|
||||
return handleGetAncModeStateResponse(response);
|
||||
case 0x02:
|
||||
case 0x04:
|
||||
return getIntResponseStatus(response);
|
||||
case 0x03:
|
||||
return handleGetVptLevelResponse(response);
|
||||
case 0x05:
|
||||
return handleGetVptEnabledResponse(response);
|
||||
case 0x06:
|
||||
return getBooleanResponseStatus(response);
|
||||
}
|
||||
} else if (response.namespace == 0x05) {
|
||||
if (response.commandId == 0x01) {
|
||||
return handleDeviceNameResponse(response);
|
||||
}
|
||||
} else if (response.namespace == 0x08) {
|
||||
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;
|
||||
}
|
||||
|
||||
private boolean handleGetAncModeStateResponse(BandWPSeriesResponse response) {
|
||||
if (!response.messageType.hasPayload) {
|
||||
GB.toast("No payload in response!", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
int payloadValue;
|
||||
try {
|
||||
payloadValue = response.payloadUnpacker.unpackInt();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Could not extract ancMode from payload: " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
|
||||
editor.putBoolean(PREF_ACTIVE_NOISE_CANCELLING_TOGGLE, payloadValue == ANC_MODE_ON);
|
||||
editor.apply();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleBatteryLevels(BandWPSeriesResponse response) {
|
||||
int[] levels = response.getPayloadFixArray();
|
||||
if (levels == null) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < levels.length; i++) {
|
||||
if (i >= 3) {
|
||||
break;
|
||||
}
|
||||
int level = (levels[i] == 0xff) ? BATTERY_UNKNOWN : levels[i];
|
||||
LOG.debug("Battery {} has level {}", i, levels[i]);
|
||||
batteryInfo[i].level = level;
|
||||
handleGBDeviceEvent(batteryInfo[i]);
|
||||
}
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
String[] versions = firmwareString.split("\\(");
|
||||
String main_version = versions[0];
|
||||
String sub_version = versions[1].substring(0, versions[1].length()-1);
|
||||
GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo();
|
||||
versionInfo.fwVersion = main_version;
|
||||
versionInfo.fwVersion2 = sub_version;
|
||||
LOG.debug("Got firmware version {}/{}", main_version, sub_version);
|
||||
handleGBDeviceEvent(versionInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleDeviceNameResponse(BandWPSeriesResponse response) {
|
||||
String deviceName = response.getPayloadString();
|
||||
if (deviceName == null) {
|
||||
return false;
|
||||
}
|
||||
getDevice().setName(deviceName);
|
||||
LOG.debug("Set device name to {}", deviceName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleGetVptEnabledResponse(BandWPSeriesResponse response) {
|
||||
if (!response.messageType.hasPayload) {
|
||||
GB.toast("No payload in response!", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
boolean payloadValue;
|
||||
try {
|
||||
payloadValue = response.payloadUnpacker.unpackBoolean();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Could not extract vptEnabled from payload: " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
int vptLevel = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getInt(PREF_BANDW_PSERIES_VPT_LEVEL, 0);
|
||||
Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
|
||||
editor.putBoolean(PREF_BANDW_PSERIES_VPT_ENABLED, payloadValue);
|
||||
editor.putInt(PREF_BANDW_PSERIES_GUI_VPT_LEVEL, payloadValue ? vptLevel + 1 : 0);
|
||||
editor.apply();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleGetVptLevelResponse(BandWPSeriesResponse response) {
|
||||
if (!response.messageType.hasPayload) {
|
||||
GB.toast("No payload in response!", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
int payloadValue;
|
||||
try {
|
||||
payloadValue = response.payloadUnpacker.unpackInt();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Could not extract vptLevel from payload: " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
boolean vptEnabled = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(PREF_BANDW_PSERIES_VPT_ENABLED, false);
|
||||
Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
|
||||
editor.putInt(PREF_BANDW_PSERIES_VPT_LEVEL, payloadValue);
|
||||
editor.putInt(PREF_BANDW_PSERIES_GUI_VPT_LEVEL, vptEnabled ? payloadValue + 1 : 0);
|
||||
editor.apply();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onSendConfiguration(String config) {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("sendConfig");
|
||||
switch (config) {
|
||||
case PREF_ACTIVE_NOISE_CANCELLING_TOGGLE:
|
||||
boolean ancMode = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(PREF_ACTIVE_NOISE_CANCELLING_TOGGLE, true);
|
||||
BandWBLEProfile.setAncModeState(builder, ancMode);
|
||||
break;
|
||||
case PREF_BANDW_PSERIES_GUI_VPT_LEVEL:
|
||||
int level = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getInt(PREF_BANDW_PSERIES_GUI_VPT_LEVEL, 0);
|
||||
BandWBLEProfile.setVptEnabled(builder, level != 0);
|
||||
if (level != 0) {
|
||||
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) {
|
||||
GB.toast("Failed to send settings update", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean getBooleanResponseStatus(BandWPSeriesResponse response) {
|
||||
boolean payloadValue;
|
||||
try {
|
||||
payloadValue = response.payloadUnpacker.unpackBoolean();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Could not extract response from payload: " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
return payloadValue;
|
||||
}
|
||||
|
||||
private boolean getIntResponseStatus(BandWPSeriesResponse response) {
|
||||
int payloadValue;
|
||||
try {
|
||||
payloadValue = response.payloadUnpacker.unpackInt();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Could not extract response from payload: " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
return payloadValue == 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
|
||||
|
||||
import org.msgpack.core.MessageBufferPacker;
|
||||
import org.msgpack.core.MessagePack;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class BandWPSeriesRequest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BandWPSeriesRequest.class);
|
||||
|
||||
BandWMessageType messageType;
|
||||
final byte namespace;
|
||||
final byte commandId;
|
||||
|
||||
private final MessageBufferPacker payloadPacker = MessagePack.newDefaultBufferPacker();
|
||||
|
||||
public BandWPSeriesRequest(byte mNamespace, byte mCommandId) throws IOException {
|
||||
messageType = BandWMessageType.REQUEST_WITHOUT_PAYLOAD;
|
||||
namespace = mNamespace;
|
||||
commandId = mCommandId;
|
||||
}
|
||||
|
||||
public BandWPSeriesRequest addToPayload(int value) throws IOException {
|
||||
payloadPacker.packInt(value);
|
||||
messageType = BandWMessageType.REQUEST_WITH_PAYLOAD;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BandWPSeriesRequest addToPayload(byte value) throws IOException {
|
||||
payloadPacker.packByte(value);
|
||||
messageType = BandWMessageType.REQUEST_WITH_PAYLOAD;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BandWPSeriesRequest addToPayload(boolean value) throws IOException {
|
||||
payloadPacker.packBoolean(value);
|
||||
messageType = BandWMessageType.REQUEST_WITH_PAYLOAD;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BandWPSeriesRequest addToPayload(String value) throws IOException {
|
||||
payloadPacker.packString(value);
|
||||
messageType = BandWMessageType.REQUEST_WITH_PAYLOAD;
|
||||
return this;
|
||||
}
|
||||
|
||||
public byte[] finishAndGetBytes() {
|
||||
byte[] payload = payloadPacker.toByteArray();
|
||||
byte len = (byte) ((this.messageType == BandWMessageType.REQUEST_WITHOUT_PAYLOAD) ? 4 : 6 + payload.length);
|
||||
byte[] out = addMessageType(new byte[len+1], messageType.value);
|
||||
out[0] = len;
|
||||
out[3] = commandId;
|
||||
out[4] = namespace;
|
||||
if (messageType == BandWMessageType.REQUEST_WITH_PAYLOAD) {
|
||||
addShort(out, 5, payload.length);
|
||||
System.arraycopy(payload, 0, out, 7, payload.length);
|
||||
}
|
||||
try {
|
||||
payloadPacker.close();
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to close payloadPacker");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private byte[] addMessageType(byte[] target, int value) {
|
||||
return addShort(target, 1, value);
|
||||
}
|
||||
|
||||
private byte[] addShort(byte[] target, int position, int value) {
|
||||
byte valueLo = (byte) (value & 0xff);
|
||||
byte valueHi = (byte) (value >> 8);
|
||||
target[position] = valueLo;
|
||||
target[position+1] = valueHi;
|
||||
return target;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
|
||||
|
||||
import org.bouncycastle.shaded.util.Arrays;
|
||||
import org.msgpack.core.MessagePack;
|
||||
import org.msgpack.core.MessageUnpacker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class BandWPSeriesResponse {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BandWPSeriesResponse.class);
|
||||
|
||||
BandWMessageType messageType;
|
||||
final byte namespace;
|
||||
final byte commandId;
|
||||
final int errorCode;
|
||||
final int payloadLength;
|
||||
final byte[] payload;
|
||||
|
||||
public final MessageUnpacker payloadUnpacker;
|
||||
|
||||
BandWPSeriesResponse(byte[] contents) {
|
||||
messageType = BandWMessageType.getByType(getUInt16(Arrays.copyOfRange(contents, 0, 2)));
|
||||
commandId = contents[2];
|
||||
namespace = contents[3];
|
||||
int payloadOffset = 6;
|
||||
if (messageType == BandWMessageType.RESPONSE_WITH_PAYLOAD || messageType == BandWMessageType.RESPONSE_WITHOUT_PAYLOAD) {
|
||||
errorCode = getUInt16(Arrays.copyOfRange(contents, 4, 6));
|
||||
} else {
|
||||
errorCode = 0;
|
||||
payloadOffset = 4;
|
||||
}
|
||||
if (messageType == null || !messageType.hasPayload || errorCode != 0) {
|
||||
payloadLength = 0;
|
||||
payload = null;
|
||||
payloadUnpacker = null;
|
||||
} else {
|
||||
payloadLength = getUInt16(Arrays.copyOfRange(contents, payloadOffset, payloadOffset + 2));
|
||||
payload = Arrays.copyOfRange(contents, payloadOffset + 2, contents.length);
|
||||
payloadUnpacker = MessagePack.newDefaultUnpacker(payload);
|
||||
}
|
||||
}
|
||||
|
||||
private int getUInt16(byte[] buffer) {
|
||||
return (0xff & buffer[0]) | ((0xff & buffer[1]) << 8);
|
||||
}
|
||||
|
||||
public String getPayloadString() {
|
||||
String value;
|
||||
try {
|
||||
value = payloadUnpacker.unpackString();
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to unpack String from payload {}", payload);
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public int[] getPayloadFixArray() {
|
||||
int length;
|
||||
try {
|
||||
length = payloadUnpacker.unpackArrayHeader();
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to unpack ArrayHeader from payload {}", payload);
|
||||
return null;
|
||||
}
|
||||
int[] values = new int[length];
|
||||
try {
|
||||
for (int i = 0; i < length; i++) {
|
||||
values[i] = payloadUnpacker.unpackInt();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to unpack byte from fixarray in payload {}", payload);
|
||||
return null;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
public boolean getPayloadBoolean() throws IOException{
|
||||
return payloadUnpacker.unpackBoolean();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -232,7 +232,7 @@ public class HuaweiEphemerisManager {
|
||||
LOG.info("Ephemeris Time: {} ConfigData: {}", fileTime, availableDataConfig.toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.error("Ephemeris exception file or config processing", e);
|
||||
LOG.info("Ephemeris exception file or config processing: {}", e.getMessage());
|
||||
availableDataConfig = null;
|
||||
//responseCode = 100007; //no network connection
|
||||
return; // NOTE: just ignore request if something wrong with data.
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -31,8 +31,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@ -52,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;
|
||||
@ -70,6 +69,10 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataValues;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataValuesDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSample;
|
||||
@ -105,6 +108,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PCalendarService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PTrackService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PDataDictionarySyncService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.AcceptAgreementsRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetAppInfoParams;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetContactsCount;
|
||||
@ -121,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;
|
||||
@ -830,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));
|
||||
@ -887,6 +893,10 @@ public class HuaweiSupportProvider {
|
||||
trackService.register();
|
||||
}
|
||||
}
|
||||
if (HuaweiP2PDataDictionarySyncService.getRegisteredInstance(huaweiP2PManager) == null) {
|
||||
HuaweiP2PDataDictionarySyncService trackService = new HuaweiP2PDataDictionarySyncService(huaweiP2PManager);
|
||||
trackService.register();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1184,6 +1194,7 @@ public class HuaweiSupportProvider {
|
||||
|
||||
private void fetchActivityData() {
|
||||
syncState.setActivitySync(true);
|
||||
fetchActivityDataP2P();
|
||||
|
||||
int sleepStart = 0;
|
||||
int stepStart = 0;
|
||||
@ -1242,6 +1253,7 @@ public class HuaweiSupportProvider {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
getStepDataCountRequest.setFinalizeReq(new RequestCallback() {
|
||||
@Override
|
||||
public void call() {
|
||||
@ -1286,6 +1298,18 @@ public class HuaweiSupportProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchActivityDataP2P() {
|
||||
HuaweiP2PDataDictionarySyncService P2PSyncService = HuaweiP2PDataDictionarySyncService.getRegisteredInstance(huaweiP2PManager);
|
||||
if (P2PSyncService != null && getHuaweiCoordinator().supportsTemperature()) {
|
||||
P2PSyncService.sendSyncRequest(400012, new HuaweiP2PDataDictionarySyncService.DictionarySyncCallback() {
|
||||
@Override
|
||||
public void onComplete(boolean complete) {
|
||||
LOG.info("Sync P2P Temperature complete");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchWorkoutData() {
|
||||
syncState.setWorkoutSync(true);
|
||||
|
||||
@ -1721,7 +1745,7 @@ public class HuaweiSupportProvider {
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
HuaweiWorkoutPaceSampleDao dao = db.getDaoSession().getHuaweiWorkoutPaceSampleDao();
|
||||
|
||||
if(number == 0) {
|
||||
if (number == 0) {
|
||||
final DeleteQuery<HuaweiWorkoutPaceSample> tableDeleteQuery = dao.queryBuilder()
|
||||
.where(HuaweiWorkoutPaceSampleDao.Properties.WorkoutId.eq(workoutId))
|
||||
.buildDelete();
|
||||
@ -1756,7 +1780,7 @@ public class HuaweiSupportProvider {
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
HuaweiWorkoutSwimSegmentsSampleDao dao = db.getDaoSession().getHuaweiWorkoutSwimSegmentsSampleDao();
|
||||
|
||||
if(number == 0) {
|
||||
if (number == 0) {
|
||||
final DeleteQuery<HuaweiWorkoutSwimSegmentsSample> tableDeleteQuery = dao.queryBuilder()
|
||||
.where(HuaweiWorkoutSwimSegmentsSampleDao.Properties.WorkoutId.eq(workoutId))
|
||||
.buildDelete();
|
||||
@ -1785,6 +1809,92 @@ public class HuaweiSupportProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public void addDictData(List<HuaweiP2PDataDictionarySyncService.DictData> dictData) {
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
|
||||
Long deviceId = DBHelper.getDevice(gbDevice, db.getDaoSession()).getId();
|
||||
|
||||
for (HuaweiP2PDataDictionarySyncService.DictData data : dictData) {
|
||||
// Avoid duplicates
|
||||
QueryBuilder<HuaweiDictData> qb = db.getDaoSession().getHuaweiDictDataDao().queryBuilder().where(
|
||||
HuaweiDictDataDao.Properties.UserId.eq(userId),
|
||||
HuaweiDictDataDao.Properties.DeviceId.eq(deviceId),
|
||||
HuaweiDictDataDao.Properties.DictClass.eq(data.getDictClass()),
|
||||
HuaweiDictDataDao.Properties.StartTimestamp.eq(data.getStartTimestamp())
|
||||
);
|
||||
List<HuaweiDictData> results = qb.build().list();
|
||||
Long dictId = null;
|
||||
if (!results.isEmpty())
|
||||
dictId = results.get(0).getDictId();
|
||||
|
||||
HuaweiDictData dictSample = new HuaweiDictData(
|
||||
dictId,
|
||||
deviceId,
|
||||
userId,
|
||||
data.getDictClass(),
|
||||
data.getStartTimestamp(),
|
||||
data.getEndTimestamp(),
|
||||
data.getModifyTimestamp()
|
||||
);
|
||||
db.getDaoSession().getHuaweiDictDataDao().insertOrReplace(dictSample);
|
||||
addDictDataValue(dictSample.getDictId(), data.getData());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to add dict data", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addDictDataValue(Long dictId, List<HuaweiP2PDataDictionarySyncService.DictData.DictDataValue> dictDataValues) {
|
||||
if (dictId == null)
|
||||
return;
|
||||
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
HuaweiDictDataValuesDao dao = db.getDaoSession().getHuaweiDictDataValuesDao();
|
||||
|
||||
for (HuaweiP2PDataDictionarySyncService.DictData.DictDataValue dataValues : dictDataValues) {
|
||||
|
||||
HuaweiDictDataValues dictValue = new HuaweiDictDataValues(
|
||||
dictId,
|
||||
dataValues.getDataType(),
|
||||
dataValues.getTag(),
|
||||
dataValues.getValue()
|
||||
);
|
||||
dao.insertOrReplace(dictValue);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to add dict value to database", e);
|
||||
}
|
||||
}
|
||||
|
||||
public long getLastDataDictLastTimestamp(int dictClass) {
|
||||
long lastTimestamp = 0;
|
||||
if (dictClass == 0)
|
||||
return lastTimestamp;
|
||||
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
|
||||
Long deviceId = DBHelper.getDevice(gbDevice, db.getDaoSession()).getId();
|
||||
|
||||
QueryBuilder<HuaweiDictData> qb = db.getDaoSession().getHuaweiDictDataDao().queryBuilder().where(
|
||||
HuaweiDictDataDao.Properties.UserId.eq(userId),
|
||||
HuaweiDictDataDao.Properties.DeviceId.eq(deviceId),
|
||||
HuaweiDictDataDao.Properties.DictClass.eq(dictClass)
|
||||
);
|
||||
List<HuaweiDictData> results = qb.build().list();
|
||||
for (HuaweiDictData data : results) {
|
||||
if (data.getModifyTimestamp() != null) {
|
||||
lastTimestamp = Math.max(lastTimestamp, data.getModifyTimestamp());
|
||||
}
|
||||
if (data.getEndTimestamp() != null) {
|
||||
lastTimestamp = Math.max(lastTimestamp, data.getEndTimestamp());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to select last timestsmp value to database", e);
|
||||
}
|
||||
return lastTimestamp;
|
||||
}
|
||||
|
||||
|
||||
public void setWearLocation() {
|
||||
try {
|
||||
@ -2021,7 +2131,7 @@ public class HuaweiSupportProvider {
|
||||
|
||||
HuaweiUploadManager.FileUploadInfo fileInfo = new HuaweiUploadManager.FileUploadInfo();
|
||||
|
||||
if(huaweiFwHelper.isMusic()) {
|
||||
if (huaweiFwHelper.isMusic()) {
|
||||
getHuaweiMusicManager().addUploadMusic(huaweiFwHelper.getMusicInfo());
|
||||
}
|
||||
|
||||
@ -2422,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);
|
||||
}
|
||||
}
|
||||
|
@ -277,6 +277,7 @@ public class HuaweiWeatherManager {
|
||||
if (response.getTlv().getInteger(0x7f, -1) == 0x000186AA) {
|
||||
// Send weather
|
||||
final ArrayList<WeatherSpec> specs = new ArrayList<>(nodomain.freeyourgadget.gadgetbridge.model.Weather.getInstance().getWeatherSpecs());
|
||||
// TODO: could be empty, not really an issue but we need to check what to send back in that case
|
||||
this.sendWeather(specs.get(0));
|
||||
return;
|
||||
}
|
||||
|
@ -36,10 +36,13 @@ import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableRowEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSportHRZones;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
@ -58,6 +61,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
@ -119,6 +123,7 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
|
||||
INDOOR_CYCLE(7, ActivityKind.INDOOR_CYCLING),
|
||||
OPEN_WATER_SWIM(8, ActivityKind.SWIMMING_OPENWATER),
|
||||
INDOOR_WALK(13, ActivityKind.INDOOR_WALKING),
|
||||
HIKING(14, ActivityKind.HIKING),
|
||||
JUMP_ROPING(21, ActivityKind.JUMP_ROPING),
|
||||
PING_PONG(128, ActivityKind.PINGPONG),
|
||||
BADMINTON(129, ActivityKind.BADMINTON),
|
||||
@ -486,7 +491,36 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
|
||||
int sumAltitudeUp = 0;
|
||||
int sumAltitudeDown = 0;
|
||||
|
||||
//NOTE: The method of retrieving HR zones from the Huawei watch is not discovered. It may not return zones.
|
||||
// So they are calculated based on config. Enabled only for running and walking activities for testing.
|
||||
// Currently only calculated zones based on MHR.
|
||||
// TODO: Use other methods after the configuration will be implemented. Use calculateMethod on HeartRateZonesConfig class.
|
||||
// TODO: Enable for other workout types
|
||||
HeartRateZonesConfig HRZonesCfg = null;
|
||||
if( type == ActivityKind.WALKING || type == ActivityKind.RUNNING) {
|
||||
ActivityUser activityUser = new ActivityUser();
|
||||
HuaweiSportHRZones hrSportZones = new HuaweiSportHRZones(activityUser.getAge());
|
||||
HRZonesCfg = hrSportZones.getHRZonesConfigByType(HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
}
|
||||
|
||||
int dataDelta = 5;
|
||||
if (dataSamples.size() >= 2 && dataSamples.get(1).getTimestamp() - dataSamples.get(0).getTimestamp() >= 40) {
|
||||
dataDelta = 60;
|
||||
}
|
||||
|
||||
int[] HRZones = new int[5];
|
||||
|
||||
int dataIdx = 0;
|
||||
for (HuaweiWorkoutDataSample dataSample : dataSamples) {
|
||||
|
||||
if(HRZonesCfg != null) {
|
||||
int zoneIdx = HRZonesCfg.getMHRZone(dataSample.getHeartRate() & 0xFF);
|
||||
if (zoneIdx != -1 && dataIdx < (dataSamples.size() - 1)) {
|
||||
HRZones[zoneIdx] += dataDelta;
|
||||
}
|
||||
dataIdx++;
|
||||
}
|
||||
|
||||
if (dataSample.getSpeed() != -1) {
|
||||
speed += dataSample.getSpeed();
|
||||
speedCount += 1;
|
||||
@ -589,6 +623,25 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
|
||||
unknownData = true;
|
||||
}
|
||||
|
||||
|
||||
if(HRZonesCfg != null) {
|
||||
final double totalTime = Arrays.stream(HRZones).sum();
|
||||
final List<String> zoneOrder = Arrays.asList(ActivitySummaryEntries.HR_ZONE_WARM_UP, ActivitySummaryEntries.HR_ZONE_FAT_BURN, ActivitySummaryEntries.HR_ZONE_AEROBIC, ActivitySummaryEntries.HR_ZONE_ANAEROBIC, ActivitySummaryEntries.HR_ZONE_EXTREME);
|
||||
for (int i = 0; i < zoneOrder.size(); i++) {
|
||||
double timeInZone = HRZones[i];
|
||||
LOG.info("Zone: {} {}", zoneOrder.get(i), timeInZone);
|
||||
summaryData.add(
|
||||
zoneOrder.get(i),
|
||||
new ActivitySummaryProgressEntry(
|
||||
timeInZone,
|
||||
ActivitySummaryEntries.UNIT_SECONDS,
|
||||
(int) (100 * timeInZone / totalTime)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Average the things that should be averaged
|
||||
if (speedCount > 0)
|
||||
speed = speed / speedCount;
|
||||
@ -749,6 +802,7 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
|
||||
summaryData.add(ActivitySummaryEntries.ELEVATION_LOSS, elevationLoss / 10.0f, ActivitySummaryEntries.UNIT_METERS);
|
||||
}
|
||||
|
||||
|
||||
final LinkedHashMap<String, ActivitySummaryTableRowEntry> pacesTable = new LinkedHashMap<>();
|
||||
|
||||
pacesTable.put("paces_table",
|
||||
|
@ -0,0 +1,279 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiP2PManager;
|
||||
|
||||
public class HuaweiP2PDataDictionarySyncService extends HuaweiBaseP2PService {
|
||||
private final Logger LOG = LoggerFactory.getLogger(HuaweiP2PDataDictionarySyncService.class);
|
||||
|
||||
public static final String MODULE = "hw.unitedevice.datadictionarysync";
|
||||
|
||||
private AtomicBoolean serviceAvailable = new AtomicBoolean(false);
|
||||
|
||||
public interface DictionarySyncCallback {
|
||||
void onComplete(boolean complete);
|
||||
}
|
||||
|
||||
private final Map<Integer, DictionarySyncCallback> currentRequests = new HashMap<>();
|
||||
|
||||
public HuaweiP2PDataDictionarySyncService(HuaweiP2PManager manager) {
|
||||
super(manager);
|
||||
LOG.info("P2PDataDictionarySyncService");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModule() {
|
||||
return HuaweiP2PDataDictionarySyncService.MODULE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPackage() {
|
||||
return "hw.watch.health.filesync";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFingerprint() {
|
||||
return "SystemApp";
|
||||
}
|
||||
|
||||
public static byte[] dictToBytes(int value) {
|
||||
return new byte[]{
|
||||
(byte) (value >>> 16),
|
||||
(byte) (value >>> 8),
|
||||
(byte) value};
|
||||
}
|
||||
|
||||
public void sendSyncRequest(int dictClass, DictionarySyncCallback callback) {
|
||||
|
||||
if (!serviceAvailable.get()) {
|
||||
LOG.info("P2PDataDictionarySyncService not available");
|
||||
callback.onComplete(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentRequests.containsKey(dictClass)) {
|
||||
LOG.info("P2PDataDictionarySyncService current class in progress");
|
||||
callback.onComplete(false);
|
||||
return;
|
||||
}
|
||||
|
||||
long startTime = manager.getSupportProvider().getLastDataDictLastTimestamp(dictClass);
|
||||
if(startTime > 0) {
|
||||
startTime += 1000;
|
||||
}
|
||||
|
||||
HuaweiTLV tlv = new HuaweiTLV()
|
||||
.put(0x1, (byte) 1)
|
||||
.put(0x2, dictToBytes(dictClass)) //-- skin temperature
|
||||
.put(0x5, Long.valueOf(startTime))
|
||||
.put(0x6, Long.valueOf(System.currentTimeMillis()))
|
||||
.put(0x0d, (byte) 1);
|
||||
byte[] data = tlv.serialize();
|
||||
if (data == null) {
|
||||
LOG.error("Incorrect data");
|
||||
callback.onComplete(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ByteBuffer packet = ByteBuffer.allocate(1 + data.length);
|
||||
packet.put((byte) 0x1); // type tlv
|
||||
packet.put(data);
|
||||
packet.flip();
|
||||
|
||||
LOG.info("P2PDataDictionarySyncService send command");
|
||||
currentRequests.put(dictClass, callback);
|
||||
|
||||
sendCommand(packet.array(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registered() {
|
||||
sendPing(new HuaweiP2PCallback() {
|
||||
@Override
|
||||
public void onResponse(int code, byte[] data) {
|
||||
if ((byte) code != (byte) 0xca)
|
||||
return;
|
||||
serviceAvailable.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister() {
|
||||
serviceAvailable.set(false);
|
||||
}
|
||||
|
||||
public static class DictData {
|
||||
public static class DictDataValue {
|
||||
private final int dataType;
|
||||
private final byte tag;
|
||||
private final byte[] value;
|
||||
|
||||
public DictDataValue(int dataType, byte tag, byte[] value) {
|
||||
this.dataType = dataType;
|
||||
this.tag = tag;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getDataType() {
|
||||
return dataType;
|
||||
}
|
||||
|
||||
public byte getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public byte[] getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuffer sb = new StringBuffer("HuaweiDictDataValue{");
|
||||
sb.append("dataType=").append(dataType);
|
||||
sb.append(", tag=").append(tag);
|
||||
sb.append(", value=");
|
||||
if (value == null) sb.append("null");
|
||||
else {
|
||||
sb.append('[');
|
||||
for (int i = 0; i < value.length; ++i)
|
||||
sb.append(i == 0 ? "" : ", ").append(value[i]);
|
||||
sb.append(']');
|
||||
}
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final int dictClass;
|
||||
private final long startTimestamp;
|
||||
private final long endTimestamp;
|
||||
private final long modifyTimestamp;
|
||||
private final List<DictDataValue> data;
|
||||
|
||||
public DictData(int dictClass, long startTimestamp, long endTimestamp, long modifyTimestamp, List<DictDataValue> data) {
|
||||
this.dictClass = dictClass;
|
||||
this.startTimestamp = startTimestamp;
|
||||
this.endTimestamp = endTimestamp;
|
||||
this.modifyTimestamp = modifyTimestamp;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int getDictClass() { return dictClass; }
|
||||
|
||||
public long getStartTimestamp() {
|
||||
return startTimestamp;
|
||||
}
|
||||
|
||||
public long getEndTimestamp() {
|
||||
return endTimestamp;
|
||||
}
|
||||
|
||||
public long getModifyTimestamp() {
|
||||
return modifyTimestamp;
|
||||
}
|
||||
|
||||
public List<DictDataValue> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuffer sb = new StringBuffer("HuaweiDictSample{");
|
||||
sb.append("startTime=").append(startTimestamp);
|
||||
sb.append(", endTime=").append(endTimestamp);
|
||||
sb.append(", modifyTime=").append(modifyTimestamp);
|
||||
sb.append(", data=").append(data);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleData(byte[] data) {
|
||||
LOG.info("P2PDataDictionarySyncService handleData: {}", data.length);
|
||||
if (data[0] == 1) {
|
||||
DictionarySyncCallback callback = null;
|
||||
try {
|
||||
|
||||
|
||||
HuaweiTLV tlv = new HuaweiTLV();
|
||||
tlv.parse(data, 1, data.length - 1);
|
||||
|
||||
int operation = tlv.getInteger(0x01); ///???
|
||||
int dictClass = tlv.getInteger(0x02);
|
||||
|
||||
if(!currentRequests.containsKey(dictClass)) {
|
||||
return;
|
||||
}
|
||||
callback = currentRequests.remove(dictClass);
|
||||
|
||||
if(callback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//NOTE: all tags with high bit set should be parsed as container
|
||||
|
||||
List<DictData> result = new ArrayList<>();
|
||||
|
||||
for (HuaweiTLV blockTlv : tlv.getObjects(0x83)) {
|
||||
for (HuaweiTLV l : blockTlv.getObjects(0x84)) {
|
||||
//5 - start time, 6 - end time, 0xc - modify time
|
||||
long startTimestamp = l.getLong(0x5);
|
||||
long endTimestamp = 0;
|
||||
long modifyTimestamp = 0;
|
||||
if (l.contains(0x6))
|
||||
endTimestamp = l.getLong(0x6);
|
||||
if (l.contains(0xc))
|
||||
modifyTimestamp = l.getLong(0xc);
|
||||
List<DictData.DictDataValue> dataValues = new ArrayList<>();
|
||||
for (HuaweiTLV l1 : l.getObjects(0x87)) {
|
||||
for (HuaweiTLV ll : l1.getObjects(0x88)) {
|
||||
int type = ll.getInteger(0x9);
|
||||
// 10 - Double - data
|
||||
// 11 - String - metadata
|
||||
if (ll.contains(0xa))
|
||||
dataValues.add(new DictData.DictDataValue(type, (byte) 0xa, ll.getBytes(0xa)));
|
||||
if (ll.contains(0xb))
|
||||
dataValues.add(new DictData.DictDataValue(type, (byte) 0xb, ll.getBytes(0xb)));
|
||||
}
|
||||
}
|
||||
result.add(new DictData(dictClass, startTimestamp, endTimestamp, modifyTimestamp, dataValues));
|
||||
}
|
||||
}
|
||||
|
||||
manager.getSupportProvider().addDictData(result);
|
||||
|
||||
if (!result.isEmpty()) {
|
||||
sendSyncRequest(dictClass, callback);
|
||||
} else {
|
||||
callback.onComplete(true);
|
||||
}
|
||||
} catch (HuaweiPacket.MissingTagException e) {
|
||||
LOG.error("P2PDataDictionarySyncService parse error", e);
|
||||
if(callback != null) {
|
||||
callback.onComplete(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static HuaweiP2PDataDictionarySyncService getRegisteredInstance(HuaweiP2PManager manager) {
|
||||
return (HuaweiP2PDataDictionarySyncService) manager.getRegisteredService(HuaweiP2PDataDictionarySyncService.MODULE);
|
||||
}
|
||||
|
||||
}
|
@ -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,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.service.devices.oppo;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
|
||||
|
||||
public class OppoHeadphonesIoThread extends BtClassicIoThread {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OppoHeadphonesIoThread.class);
|
||||
|
||||
private final OppoHeadphonesProtocol mProtocol;
|
||||
|
||||
public OppoHeadphonesIoThread(final GBDevice gbDevice,
|
||||
final Context context,
|
||||
final OppoHeadphonesProtocol deviceProtocol,
|
||||
final AbstractSerialDeviceSupport deviceSupport,
|
||||
final BluetoothAdapter btAdapter) {
|
||||
super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter);
|
||||
this.mProtocol = deviceProtocol;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected UUID getUuidToConnect(@NonNull final ParcelUuid[] uuids) {
|
||||
return UUID.fromString("0000079a-d102-11e1-9b23-00025b00a5a5");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize() {
|
||||
write(mProtocol.encodeFirmwareVersionReq());
|
||||
write(mProtocol.encodeConfigurationReq());
|
||||
write(mProtocol.encodeBatteryReq());
|
||||
setUpdateState(GBDevice.State.INITIALIZED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] parseIncoming(final InputStream inStream) throws IOException {
|
||||
final byte[] buffer = new byte[1048576]; //HUGE read
|
||||
final int bytes = inStream.read(buffer);
|
||||
// FIXME: We should buffer this and handle partial commands
|
||||
LOG.debug("Read {} bytes: {}", bytes, hexdump(buffer, 0, bytes));
|
||||
return Arrays.copyOf(buffer, bytes);
|
||||
}
|
||||
}
|
@ -0,0 +1,355 @@
|
||||
/* 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.service.devices.oppo;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoHeadphonesPreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.OppoCommand;
|
||||
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.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
|
||||
|
||||
public class OppoHeadphonesProtocol extends GBDeviceProtocol {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OppoHeadphonesProtocol.class);
|
||||
|
||||
public static final byte CMD_PREAMBLE = (byte) 0xaa;
|
||||
|
||||
private int seqNum = 0;
|
||||
|
||||
protected OppoHeadphonesProtocol(final GBDevice device) {
|
||||
super(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDeviceEvent[] decodeResponse(final byte[] responseData) {
|
||||
final List<GBDeviceEvent> events = new ArrayList<>();
|
||||
int i = 0;
|
||||
while (i < responseData.length) {
|
||||
if (responseData[i] != CMD_PREAMBLE) {
|
||||
LOG.warn("Unexpected preamble {}", responseData[i]);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
final int totalLength = responseData[i + 1] & 0xff;
|
||||
if (responseData.length - i < totalLength + 2) {
|
||||
LOG.error("Got partial response with {} bytes, expected {}", responseData.length - i, totalLength + 2);
|
||||
break;
|
||||
}
|
||||
|
||||
final byte[] singleResponse = ArrayUtils.subarray(responseData, i, i + totalLength + 3);
|
||||
|
||||
events.addAll(handleSingleResponse(singleResponse));
|
||||
|
||||
i += totalLength + 2;
|
||||
}
|
||||
return events.toArray(new GBDeviceEvent[0]);
|
||||
}
|
||||
|
||||
private static List<GBDeviceEvent> handleSingleResponse(final byte[] responseData) {
|
||||
final List<GBDeviceEvent> events = new ArrayList<>();
|
||||
|
||||
final ByteBuffer responseBuf = ByteBuffer.wrap(responseData).order(ByteOrder.LITTLE_ENDIAN);
|
||||
final byte preamble = responseBuf.get();
|
||||
|
||||
if (preamble != CMD_PREAMBLE) {
|
||||
LOG.error("Unexpected preamble {}", preamble);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
final byte totalLength = responseBuf.get();
|
||||
if (responseData.length != totalLength + 2) {
|
||||
LOG.error("Invalid number of bytes {}, expected {}", responseData.length, totalLength + 2);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
final short zero = responseBuf.getShort();
|
||||
if (zero != 0 && zero != 4) {
|
||||
// 0 on oppo, 4 on realme?
|
||||
LOG.warn("Unexpected bytes: {}, expected 0 or 4", zero);
|
||||
}
|
||||
|
||||
final short code = responseBuf.getShort();
|
||||
final OppoCommand command = OppoCommand.fromCode(code);
|
||||
if (command == null) {
|
||||
LOG.warn("Unknown command code {}", String.format(Locale.ROOT, "0x%04x", code));
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
final int seq = responseBuf.get();
|
||||
final short payloadLength = responseBuf.getShort();
|
||||
final byte[] payload = new byte[payloadLength];
|
||||
responseBuf.get(payload);
|
||||
|
||||
switch (command) {
|
||||
case BATTERY_RET: {
|
||||
if (payload[0] != 0) {
|
||||
LOG.error("Unknown battery ret {}", payload[0]);
|
||||
break;
|
||||
}
|
||||
events.addAll(parseBattery(payload));
|
||||
break;
|
||||
}
|
||||
case DEVICE_INFO: {
|
||||
switch (payload[0]) {
|
||||
case 1: // battery
|
||||
events.addAll(parseBattery(payload));
|
||||
break;
|
||||
case 2: // status
|
||||
LOG.debug("Got status");
|
||||
// TODO handle
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown device info {}", payload[0]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case FIRMWARE_RET: {
|
||||
if (payload[0] != 0) {
|
||||
LOG.warn("Unexpected firmware ret {}", payload[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
final String fwString;
|
||||
if (payload[payload.length - 1] == 0) {
|
||||
fwString = new String(ArrayUtils.subarray(payload, 2, payload.length - 1)).strip();
|
||||
} else {
|
||||
fwString = new String(ArrayUtils.subarray(payload, 2, payload.length - 2)).strip();
|
||||
}
|
||||
final String[] parts = fwString.split(",");
|
||||
if (parts.length % 3 != 0) {
|
||||
LOG.warn("Fw parts length {} from '{}' is not divisible by 3", parts.length, fwString);
|
||||
break;
|
||||
}
|
||||
final String[] fwVersionParts = new String[3];
|
||||
for (int i = 0; i < parts.length; i += 3) {
|
||||
final String versionPart = parts[i];
|
||||
final String versionType = parts[i + 1];
|
||||
final String version = parts[i + 2];
|
||||
if (!"2".equals(versionType)) {
|
||||
continue; // not fw
|
||||
}
|
||||
|
||||
switch (versionPart) {
|
||||
case "1":
|
||||
fwVersionParts[0] = version;
|
||||
break;
|
||||
case "2":
|
||||
fwVersionParts[1] = version;
|
||||
break;
|
||||
case "3":
|
||||
fwVersionParts[2] = version;
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown firmware version part {}", versionPart);
|
||||
}
|
||||
}
|
||||
|
||||
final List<String> nonNullParts = new ArrayList<>(fwVersionParts.length);
|
||||
for (int i = 0; i < fwVersionParts.length; i++) {
|
||||
if (fwVersionParts[i] == null) {
|
||||
continue;
|
||||
}
|
||||
nonNullParts.add(fwVersionParts[i]);
|
||||
if (fwVersionParts[i].contains(".")) {
|
||||
// Realme devices have the version already with the dots, repeated multiple times
|
||||
break;
|
||||
}
|
||||
}
|
||||
final String fwVersion = String.join(".", nonNullParts);
|
||||
|
||||
final GBDeviceEventVersionInfo eventVersionInfo = new GBDeviceEventVersionInfo();
|
||||
eventVersionInfo.fwVersion = fwVersion;
|
||||
eventVersionInfo.hwVersion = GBApplication.getContext().getString(R.string.n_a);
|
||||
events.add(eventVersionInfo);
|
||||
|
||||
LOG.debug("Got fw version: {}", fwVersion);
|
||||
|
||||
break;
|
||||
}
|
||||
case FIND_DEVICE_ACK: {
|
||||
LOG.debug("Got find device ack, status={}", payload[0]);
|
||||
break;
|
||||
}
|
||||
case TOUCH_CONFIG_RET: {
|
||||
if (payload[0] != 0) {
|
||||
LOG.warn("Unknown config ret {}", payload[0]);
|
||||
break;
|
||||
}
|
||||
if ((payload.length - 2) % 4 != 0) {
|
||||
LOG.warn("Unexpected config ret payload size {}", payload.length);
|
||||
break;
|
||||
}
|
||||
|
||||
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences();
|
||||
|
||||
for (int i = 2; i < payload.length; i += 4) {
|
||||
final int sideCode = payload[i] & 0xff;
|
||||
final int typeCode = BLETypeConversions.toUint16(payload, i + 1);
|
||||
final int valueCode = payload[i + 3] & 0xff;
|
||||
final TouchConfigSide side = TouchConfigSide.fromCode(sideCode);
|
||||
final TouchConfigType type = TouchConfigType.fromCode(typeCode);
|
||||
final TouchConfigValue value = TouchConfigValue.fromCode(valueCode);
|
||||
|
||||
if (side == null) {
|
||||
LOG.warn("Unknown side code {}", sideCode);
|
||||
continue;
|
||||
}
|
||||
if (type == null) {
|
||||
LOG.warn("Unknown type code {}", typeCode);
|
||||
continue;
|
||||
}
|
||||
if (value == null) {
|
||||
LOG.warn("Unknown value code {}", valueCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG.debug("Got touch config for {} {} = {}", side, type, value);
|
||||
|
||||
eventUpdatePreferences.withPreference(
|
||||
OppoHeadphonesPreferences.getKey(side, type),
|
||||
value.name().toLowerCase(Locale.ROOT)
|
||||
);
|
||||
}
|
||||
|
||||
events.add(eventUpdatePreferences);
|
||||
|
||||
break;
|
||||
}
|
||||
case TOUCH_CONFIG_ACK: {
|
||||
LOG.debug("Got config ack, status={}", payload[0]);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG.warn("Unhandled command {}", command);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
private static List<GBDeviceEvent> parseBattery(final byte[] payload) {
|
||||
final List<GBDeviceEvent> events = new ArrayList<>();
|
||||
|
||||
final int numBatteries = payload[1] & 0xff;
|
||||
for (int i = 2; i < payload.length; i += 2) {
|
||||
if ((payload[i] & 0xff) == 0xff) {
|
||||
continue;
|
||||
}
|
||||
final int batteryIndex = payload[i] - 1;
|
||||
if (batteryIndex < 0 || batteryIndex > 2) {
|
||||
LOG.error("Unknown battery index {}", payload[i]);
|
||||
break;
|
||||
}
|
||||
|
||||
final int batteryLevel = payload[i + 1] & 0x7f;
|
||||
final BatteryState batteryState = (payload[i + 1] & 0x80) != 0 ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL;
|
||||
|
||||
LOG.debug("Got battery {}: {}%, {}", batteryIndex, batteryLevel, batteryState);
|
||||
|
||||
final GBDeviceEventBatteryInfo eventBatteryInfo = new GBDeviceEventBatteryInfo();
|
||||
eventBatteryInfo.batteryIndex = batteryIndex;
|
||||
eventBatteryInfo.level = batteryLevel;
|
||||
eventBatteryInfo.state = batteryState;
|
||||
events.add(eventBatteryInfo);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeFirmwareVersionReq() {
|
||||
return encodeMessage(OppoCommand.FIRMWARE_GET, new byte[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeFindDevice(final boolean start) {
|
||||
return encodeMessage(OppoCommand.FIND_DEVICE_REQ, new byte[]{(byte) (start ? 0x01 : 0x00)});
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeSendConfiguration(final String config) {
|
||||
final DevicePrefs prefs = getDevicePrefs();
|
||||
|
||||
if (config.startsWith("oppo_touch__")) {
|
||||
final String[] parts = config.split("__");
|
||||
final TouchConfigSide side = TouchConfigSide.valueOf(parts[1].toUpperCase(Locale.ROOT));
|
||||
final TouchConfigType type = TouchConfigType.valueOf(parts[2].toUpperCase(Locale.ROOT));
|
||||
final String valueCode = prefs.getString(OppoHeadphonesPreferences.getKey(side, type), null);
|
||||
if (valueCode == null) {
|
||||
LOG.warn("Failed to get touch option value for {}/{}", side, type);
|
||||
return super.encodeSendConfiguration(config);
|
||||
}
|
||||
|
||||
final TouchConfigValue value = TouchConfigValue.valueOf(valueCode.toUpperCase(Locale.ROOT));
|
||||
|
||||
LOG.debug("Sending {} {} = {}", side, type, value);
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(5).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put((byte) 0x01);
|
||||
buf.put((byte) side.getCode());
|
||||
buf.putShort((short) type.getCode());
|
||||
buf.put((byte) value.getCode());
|
||||
|
||||
return encodeMessage(OppoCommand.TOUCH_CONFIG_SET, buf.array());
|
||||
}
|
||||
|
||||
return super.encodeSendConfiguration(config);
|
||||
}
|
||||
|
||||
public byte[] encodeBatteryReq() {
|
||||
return encodeMessage(OppoCommand.BATTERY_REQ, new byte[0]);
|
||||
}
|
||||
|
||||
public byte[] encodeConfigurationReq() {
|
||||
return encodeMessage(OppoCommand.TOUCH_CONFIG_REQ, new byte[]{0x02, 0x03, 0x01});
|
||||
}
|
||||
|
||||
private byte[] encodeMessage(final OppoCommand command, final byte[] payload) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(9 + payload.length).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_PREAMBLE);
|
||||
buf.put((byte) (buf.limit() - 2));
|
||||
buf.put((byte) 0);
|
||||
buf.put((byte) 0);
|
||||
buf.putShort(command.getCode());
|
||||
buf.put((byte) seqNum++);
|
||||
buf.putShort((short) payload.length);
|
||||
buf.put(payload);
|
||||
return buf.array();
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/* 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.service.devices.oppo;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
|
||||
public class OppoHeadphonesSupport extends AbstractHeadphoneDeviceSupport {
|
||||
@Override
|
||||
protected GBDeviceProtocol createDeviceProtocol() {
|
||||
return new OppoHeadphonesProtocol(getDevice());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GBDeviceIoThread createDeviceIOThread() {
|
||||
return new OppoHeadphonesIoThread(
|
||||
getDevice(),
|
||||
getContext(),
|
||||
(OppoHeadphonesProtocol) getDeviceProtocol(),
|
||||
OppoHeadphonesSupport.this,
|
||||
getBluetoothAdapter()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/* 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.service.devices.oppo.commands;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public enum OppoCommand {
|
||||
BATTERY_REQ(0x0106),
|
||||
BATTERY_RET(0x8106),
|
||||
DEVICE_INFO(0x0204),
|
||||
FIRMWARE_GET(0x0105),
|
||||
FIRMWARE_RET(0x8105),
|
||||
TOUCH_CONFIG_REQ(0x0108),
|
||||
TOUCH_CONFIG_SET(0x0401),
|
||||
TOUCH_CONFIG_RET(0x8108),
|
||||
TOUCH_CONFIG_ACK(0x8401),
|
||||
FIND_DEVICE_REQ(0x0400),
|
||||
FIND_DEVICE_ACK(0x8400),
|
||||
;
|
||||
|
||||
private final short code;
|
||||
|
||||
OppoCommand(final int code) {
|
||||
this.code = (short) code;
|
||||
}
|
||||
|
||||
public short getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static OppoCommand fromCode(final short code) {
|
||||
for (final OppoCommand cmd : OppoCommand.values()) {
|
||||
if (cmd.code == code) {
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/* 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.service.devices.oppo.commands;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public enum TouchConfigSide {
|
||||
LEFT(0x01),
|
||||
RIGHT(0x02),
|
||||
BOTH(0x04),
|
||||
;
|
||||
|
||||
private final int code;
|
||||
|
||||
TouchConfigSide(final int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static TouchConfigSide fromCode(final int code) {
|
||||
for (final TouchConfigSide param : TouchConfigSide.values()) {
|
||||
if (param.code == code) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/* 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.service.devices.oppo.commands;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public enum TouchConfigType {
|
||||
UNK_1(0x0101),
|
||||
TAP_2(0x0201),
|
||||
TAP_3(0x0301),
|
||||
HOLD(0x0401),
|
||||
;
|
||||
|
||||
private final int code;
|
||||
|
||||
TouchConfigType(final int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static TouchConfigType fromCode(final int code) {
|
||||
for (final TouchConfigType param : TouchConfigType.values()) {
|
||||
if (param.code == code) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user