1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-26 20:06:52 +01:00

Compare commits

...

61 Commits

Author SHA1 Message Date
Victor Kareh
769c592940 pinetime: Add sunrise and sunset weather data 2024-11-20 12:20:34 -05:00
Renato Aguiar
212289645f Add Garmin Instinct 2 2024-11-19 20:59:05 +00:00
José Rebelo
6b5c5ae0ac Garmin: Fix weather temperature conversion to celsius 2024-11-19 20:57:04 +00:00
José Rebelo
9d1a57b6c2 Fix crash in some chart pages (#4319) 2024-11-19 20:53:34 +00:00
dependency-bot
b56ed974a3 Update dependency com.android.tools:desugar_jdk_libs to v2.1.3 2024-11-19 00:15:30 +00:00
MrYoranimo
b5bd4da9b1 Xiaomi SPPv2: Catch exception thrown in onPacketReceived
When a received packet causes an exception to be thrown while
getting handled in the service's onPacketReceived method, the
message will get stuck in the queue because it is never released.
Subsequently received messages get lined up after the first message
that causes an exception, and since that message is never removed,
those newer messages are never processed.

Catching the exception thrown from within the onPacketReceived method
allows the code flow to continue and therefore remove the troubling
message from the queue.
2024-11-18 23:25:28 +01:00
José Rebelo
1d2404a4e6 Garmin: Display AGPS age 2024-11-17 19:00:00 +00:00
Martin.JM
39e7bd8c62 Huawei: Add non-P2P HR zones configuration 2024-11-17 17:57:30 +00:00
José Rebelo
5f91715c89 Realme Buds T110: Initial support 2024-11-17 17:23:23 +00:00
José Rebelo
1618fda418 Log exceptions during DBAccess async tasks 2024-11-17 00:01:40 +00:00
José Rebelo
e453855e88 Do not suppress repeated notifications if timestamp is in the future (#4327) 2024-11-16 21:46:22 +00:00
Me7c7
dc1533b4ed Huawei: Initial music managment support 2024-11-16 20:41:23 +00:00
José Rebelo
1a3a7dec05 Prevent heart rate average from using invalid samples 2024-11-16 14:08:06 +00:00
José Rebelo
87bc2e6ed7 Fix imperial unit on steps charts 2024-11-15 23:16:40 +00:00
CaptKentish
9bd828814e Add water sports icons (#4322) 2024-11-15 22:09:44 +00:00
huyz
6aa7280967 Add some workout icons 2024-11-14 23:25:28 +00:00
José Rebelo
f16e2eeabb Test device: Add dummy activities 2024-11-14 23:22:18 +00:00
Arjan Schrijver
e83555f099 Fossil/Skagen Hybrids: Fix erroneous watchface downgrade from de37e5b6f 2024-11-14 14:10:14 +01:00
José Rebelo
9b6fce566d Mi Band 9: Fix outdoor cycling parsing 2024-11-12 23:32:11 +00:00
Andreas Shimokawa
de37e5b6fd bump version, add xml changelog 2024-11-11 23:29:47 +01:00
Arjan Schrijver
cbb710abe7 Update changelog 2024-11-11 23:27:59 +01:00
mvn23
31b8fd683d Add wear sensor toggle to Bowers and Wilkins P Series 2024-11-11 02:15:41 +01:00
José Rebelo
82f221752e Compute activity average speed 2024-11-10 22:50:58 +00:00
José Rebelo
c2c1e48c85 Update changelog 2024-11-10 22:39:55 +00:00
José Rebelo
810df3055c Garmin Forerunner 55/620: Initial support 2024-11-10 22:33:48 +00:00
José Rebelo
a72de07d2a Oppo Enco Air: Initial support 2024-11-10 22:18:41 +00:00
José Rebelo
7a0e43a4de GBDevice: Do not unset firmware from dynamic state
It is not clear why this was being done, but it is the source of issues
for multiple devices, since the ensureDeviceUpToDate function will
attempt to persist the null values, in non-nullable columns.
2024-11-10 22:16:14 +00:00
Me7c7
ce32ac7272 Huawei: Do not print exception if the ephemeris file does not exist 2024-11-09 20:04:55 +02:00
Martin.JM
2a865fe498 Huawei: Fix SmartAlarm for Huawei Watch GT
Linked to #4308.
2024-11-09 17:58:46 +00:00
Alik Aslanyan
f3185f1acb
Fix null elements for some mixed case words in Armenian (combinations with U and Vo) 2024-11-09 20:23:57 +04:00
mvn23
6bb93bef89 Add ANC and Passthrough options to Bowers and Wilkins P Series (#4297)
Co-authored-by: mvn23 <schopdiedwaas@gmail.com>
Co-committed-by: mvn23 <schopdiedwaas@gmail.com>
2024-11-09 15:29:44 +00:00
gjaekel
7c1d44fcd3 Add support for HUAWEI Band3Pro (#4296)
Co-authored-by: gjaekel <gjaekel@noreply.codeberg.org>
Co-committed-by: gjaekel <gjaekel@noreply.codeberg.org>
2024-11-09 15:20:48 +00:00
José Rebelo
a2323ce845 Withings: Fix crash on connection 2024-11-09 08:16:12 +00:00
Arjan Schrijver
5a0f1e46db Garmin Fenix 6S Pro: Initial support 2024-11-08 09:11:47 +01:00
Martin.JM
126102aa05 Add tests and a couple of fixes 2024-11-05 23:43:41 +00:00
Martin.JM
f0ffc0e165 Add high res HR to activity sleep chart fragment 2024-11-05 23:43:41 +00:00
Martin.JM
82e3a86350 Implement high res HR data
Specifically for:
- The HR fragment
- The sports activity graph

Also adds support for Huawei high res HR, and high res SpO2.
2024-11-05 23:43:41 +00:00
Martin.JM
1882ee947e Huawei: Improve distanceCm 2024-11-05 23:43:41 +00:00
Me7c7
ae84678de8 Huawei: Sync dict data over P2P. Sync skin temperature. 2024-11-05 23:42:19 +00:00
huyz
68caf6a60f Huawei: map activity type 14 to hiking activity 2024-11-04 21:26:31 +08:00
mvn23
d53971c881 Add initial Bowers and Wilkins P series support. (#4288)
Co-authored-by: mvn23 <schopdiedwaas@gmail.com>
Co-committed-by: mvn23 <schopdiedwaas@gmail.com>
2024-11-01 21:06:34 +00:00
José Rebelo
46dd45cb4e Garmin Forerunner 235: Initial support 2024-10-31 23:47:20 +00:00
José Rebelo
cd068abdd3 Update androidx.camera to v1.4.0 2024-10-31 23:44:46 +00:00
José Rebelo
aa2b6d142a Update androidx.constraintlayout to v2.2.0 2024-10-31 23:44:27 +00:00
José Rebelo
d9cc15e3c5 Update androidx.fragment:fragment to v1.8.5 2024-10-31 23:43:51 +00:00
Me7c7
a7c19c8190 Huawei: calculate HR Zones for walking and running workouts. 2024-10-31 23:14:04 +00:00
Thomas Di Cristofaro
d038c589c1
Translated using Weblate (Italian)
Currently translated at 100.0% (3295 of 3295 strings)

Translation: Freeyourgadget/Gadgetbridge
Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/it/
2024-10-31 14:40:21 +01:00
Fjuro
8df3dac11c
Translated using Weblate (Czech)
Currently translated at 100.0% (3295 of 3295 strings)

Translation: Freeyourgadget/Gadgetbridge
Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/cs/
2024-10-31 14:18:19 +01:00
Thomas Di Cristofaro
a12b56fb37
Translated using Weblate (Italian)
Currently translated at 100.0% (3294 of 3294 strings)

Translation: Freeyourgadget/Gadgetbridge
Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/it/
2024-10-30 21:31:26 +00:00
Yusuf Cihan
2315b56ff8 Add screenshot script & update F-Droid screenshots 2024-10-30 21:23:49 +00:00
José Rebelo
ed1f1735c5 Update changelogs 2024-10-30 20:51:39 +00:00
José Rebelo
292a5d11b5 Activity Charts: Update date info when refreshed 2024-10-30 20:51:02 +00:00
Zahnstocher
a6053eda77 Add support for Sony WI-C100 2024-10-30 20:47:37 +00:00
Zahnstocher
cbd9a7b8af Add voice notifications for Sony WF-C500 2024-10-30 14:31:16 +01:00
José Rebelo
29866c2b41 Fix linter 2024-10-29 11:23:12 +00:00
Andreas Shimokawa
1e0117727e fix xml 2024-10-29 11:49:59 +01:00
Andreas Shimokawa
3a152978d7 bump version, update changelog
trim changelog_master.xml to include changes for the last 2 years only
2024-10-29 11:45:00 +01:00
Thomas Di Cristofaro
7f9c571d63
Translated using Weblate (Italian)
Currently translated at 96.7% (3186 of 3294 strings)

Translation: Freeyourgadget/Gadgetbridge
Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/it/
2024-10-29 07:04:36 +01:00
Thomas Di Cristofaro
16b12e099f
Translated using Weblate (Italian)
Currently translated at 93.6% (3084 of 3294 strings)

Translation: Freeyourgadget/Gadgetbridge
Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/it/
2024-10-28 22:40:28 +01:00
arjan-s
dae338fdc5
Translated using Weblate (Dutch)
Currently translated at 100.0% (3294 of 3294 strings)

Translation: Freeyourgadget/Gadgetbridge
Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/nl/
2024-10-28 22:40:27 +01:00
a0z
a79cd94de9 HRV Status: segment detection fix 2024-10-28 22:37:54 +01:00
174 changed files with 7039 additions and 1903 deletions

View File

@ -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>

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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"

View File

@ -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);
}

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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<>();

View File

@ -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<>();

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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());

View File

@ -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) -> {

View File

@ -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;

View File

@ -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";

View File

@ -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 -> {

View File

@ -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;
}
}
}
};
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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.

View File

@ -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);
}

View File

@ -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.

View File

@ -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;

View File

@ -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
};
}
}

View File

@ -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));
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,34 @@
/* Copyright (C) 2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
public class GarminForerunner55Coordinator extends GarminCoordinator {
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("^Forerunner 55$");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_garmin_forerunner_55;
}
}

View File

@ -0,0 +1,34 @@
/* Copyright (C) 2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
public class GarminForerunner620Coordinator extends GarminCoordinator {
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("^Forerunner 620$");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_garmin_forerunner_620;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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-";

View File

@ -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

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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()));
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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)));
}};
}
}

View File

@ -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();
}

View File

@ -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)
);
}
}

View File

@ -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());
}
}
}

View File

@ -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
));
}};
}
}

View File

@ -37,6 +37,7 @@ public class SonyWFC500Coordinator extends SonyHeadphonesCoordinator {
SonyHeadphonesCapabilities.EqualizerSimple,
SonyHeadphonesCapabilities.EqualizerWithCustomBands,
SonyHeadphonesCapabilities.AudioUpsampling,
SonyHeadphonesCapabilities.VoiceNotifications,
SonyHeadphonesCapabilities.PowerOffFromPhone
);
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;
}
});
}
}

View File

@ -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
);

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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),

View File

@ -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);

View File

@ -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";

View File

@ -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),

View File

@ -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) {}
}

View File

@ -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) ?

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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",

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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