From ae84678de81817cf06f0cc2dca32e9ba91e88b4d Mon Sep 17 00:00:00 2001 From: Me7c7 Date: Sun, 3 Nov 2024 13:34:11 +0200 Subject: [PATCH] Huawei: Sync dict data over P2P. Sync skin temperature. --- .../gadgetbridge/daogen/GBDaoGenerator.java | 41 ++- .../charts/TemperatureChartFragment.java | 1 + .../devices/huawei/HuaweiBRCoordinator.java | 11 + .../devices/huawei/HuaweiCoordinator.java | 13 + .../devices/huawei/HuaweiLECoordinator.java | 11 + .../devices/huawei/HuaweiTLV.java | 4 + .../HuaweiTemperatureSampleProvider.java | 190 ++++++++++++ .../devices/huawei/HuaweiSupportProvider.java | 117 +++++++- .../HuaweiP2PDataDictionarySyncService.java | 279 ++++++++++++++++++ 9 files changed, 661 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTemperatureSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PDataDictionarySyncService.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 77da82bde..d121dfd2f 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -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); @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TemperatureChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TemperatureChartFragment.java index 1dd9e136a..7cea48350 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TemperatureChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TemperatureChartFragment.java @@ -60,6 +60,7 @@ public class TemperatureChartFragment extends AbstractChartFragment getTemperatureSampleProvider(final GBDevice device, final DaoSession session) { + return new HuaweiTemperatureSampleProvider(device, session); + } + public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { return huaweiCoordinator.getDeviceSpecificSettings(device); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java index f70a5d603..a78c16b0e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java @@ -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 qb3 = session.getHuaweiDictDataDao().queryBuilder(); + List 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() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java index 7cbc50287..854ea0d7a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java @@ -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 getTemperatureSampleProvider(final GBDevice device, final DaoSession session) { + return new HuaweiTemperatureSampleProvider(device, session); + } + public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { return huaweiCoordinator.getDeviceSpecificSettings(device); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java index 9edcf7412..687100fb2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java @@ -243,6 +243,10 @@ public class HuaweiTLV { return ByteBuffer.wrap(getBytes(tag)).getShort(); } + 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) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTemperatureSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTemperatureSampleProvider.java new file mode 100644 index 000000000..22f11e8a6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTemperatureSampleProvider.java @@ -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 { + + 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 getAllSamples(long timestampFrom, long timestampTo) { + + List 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 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 dictData = qb.build().list(); + + if (dictData.isEmpty()) + return ret; + + List ids = dictData.stream().map(HuaweiDictData::getDictId).collect(Collectors.toList()); + + QueryBuilder 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 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 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 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 data = qb.build().list(); + if (data.isEmpty()) + return null; + + + QueryBuilder 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 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 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 data = qb.build().list(); + if (data.isEmpty()) + return null; + + QueryBuilder 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 valuesData = qbv.build().list(); + + if (valuesData.isEmpty()) + return null; + + return new HuaweiTemperatureSample(valuesData.get(0).getHuaweiDictData().getStartTimestamp(), (float) conv2Double(valuesData.get(0).getValue())); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java index 0489cb5c8..735ebe5d6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java @@ -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; @@ -70,6 +68,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 +107,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; @@ -887,6 +890,10 @@ public class HuaweiSupportProvider { trackService.register(); } } + if (HuaweiP2PDataDictionarySyncService.getRegisteredInstance(huaweiP2PManager) == null) { + HuaweiP2PDataDictionarySyncService trackService = new HuaweiP2PDataDictionarySyncService(huaweiP2PManager); + trackService.register(); + } } } @@ -1184,6 +1191,7 @@ public class HuaweiSupportProvider { private void fetchActivityData() { syncState.setActivitySync(true); + fetchActivityDataP2P(); int sleepStart = 0; int stepStart = 0; @@ -1242,6 +1250,7 @@ public class HuaweiSupportProvider { } }); + getStepDataCountRequest.setFinalizeReq(new RequestCallback() { @Override public void call() { @@ -1286,6 +1295,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 +1742,7 @@ public class HuaweiSupportProvider { try (DBHandler db = GBApplication.acquireDB()) { HuaweiWorkoutPaceSampleDao dao = db.getDaoSession().getHuaweiWorkoutPaceSampleDao(); - if(number == 0) { + if (number == 0) { final DeleteQuery tableDeleteQuery = dao.queryBuilder() .where(HuaweiWorkoutPaceSampleDao.Properties.WorkoutId.eq(workoutId)) .buildDelete(); @@ -1756,7 +1777,7 @@ public class HuaweiSupportProvider { try (DBHandler db = GBApplication.acquireDB()) { HuaweiWorkoutSwimSegmentsSampleDao dao = db.getDaoSession().getHuaweiWorkoutSwimSegmentsSampleDao(); - if(number == 0) { + if (number == 0) { final DeleteQuery tableDeleteQuery = dao.queryBuilder() .where(HuaweiWorkoutSwimSegmentsSampleDao.Properties.WorkoutId.eq(workoutId)) .buildDelete(); @@ -1785,6 +1806,92 @@ public class HuaweiSupportProvider { } } + public void addDictData(List 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 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 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 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 qb = db.getDaoSession().getHuaweiDictDataDao().queryBuilder().where( + HuaweiDictDataDao.Properties.UserId.eq(userId), + HuaweiDictDataDao.Properties.DeviceId.eq(deviceId), + HuaweiDictDataDao.Properties.DictClass.eq(dictClass) + ); + List 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 +2128,7 @@ public class HuaweiSupportProvider { HuaweiUploadManager.FileUploadInfo fileInfo = new HuaweiUploadManager.FileUploadInfo(); - if(huaweiFwHelper.isMusic()) { + if (huaweiFwHelper.isMusic()) { getHuaweiMusicManager().addUploadMusic(huaweiFwHelper.getMusicInfo()); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PDataDictionarySyncService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PDataDictionarySyncService.java new file mode 100644 index 000000000..339a6058e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PDataDictionarySyncService.java @@ -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 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 data; + + public DictData(int dictClass, long startTimestamp, long endTimestamp, long modifyTimestamp, List 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 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 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 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); + } + +}