persistence: add SampleProvider support

This commit is contained in:
Thomas Kuehne
2026-05-29 01:30:58 +00:00
parent e7b86faf7a
commit 57e493fa86
23 changed files with 87 additions and 28 deletions
@@ -1,5 +1,5 @@
/* Copyright (C) 2016-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniel /* Copyright (C) 2016-2026 Andreas Shimokawa, Carsten Pfeiffer, Daniel
Dakhno, Daniele Gobbetti, José Rebelo, Petr Vaněk Dakhno, Daniele Gobbetti, José Rebelo, Petr Vaněk, Thomas Kuehne
This file is part of Gadgetbridge. This file is part of Gadgetbridge.
@@ -17,6 +17,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices; package nodomain.freeyourgadget.gadgetbridge.devices;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.toast;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -37,21 +42,25 @@ import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property; import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.QueryBuilder; import de.greenrobot.dao.query.QueryBuilder;
import de.greenrobot.dao.query.WhereCondition; import de.greenrobot.dao.query.WhereCondition;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/** /**
* Base class for all sample providers. A Sample provider is device specific and provides * Base class for all sample providers. A Sample provider is device specific and provides
* access to the device specific samples. There are both read and write operations. * access to the device specific samples. There are both read and write operations.
* @param <T> the sample type * @param <T> the sample type
*/ */
public abstract class AbstractSampleProvider<T extends AbstractActivitySample> implements SampleProvider<T> { public abstract class AbstractSampleProvider<T extends AbstractActivitySample> implements SampleProvider<T>, PersistanceProvider<T> {
private static final Logger LOG = LoggerFactory.getLogger(AbstractSampleProvider.class); private static final Logger LOG = LoggerFactory.getLogger(AbstractSampleProvider.class);
private static final WhereCondition[] NO_CONDITIONS = new WhereCondition[0]; private static final WhereCondition[] NO_CONDITIONS = new WhereCondition[0];
@@ -105,7 +114,7 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
} }
@Override @Override
public void addGBActivitySamples(T[] activitySamples) { public void addGBActivitySamples(@NonNull List<T> activitySamples) {
getSampleDao().insertOrReplaceInTx(activitySamples); getSampleDao().insertOrReplaceInTx(activitySamples);
} }
@@ -452,4 +461,47 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
dummySample.setProvider(this); dummySample.setProvider(this);
return dummySample; return dummySample;
} }
@Override
public boolean persistSamples(@NonNull final List<T> samples, @Nullable final Context context) {
if (samples.isEmpty()) {
return true;
}
LOG.debug(
"Will persist {} {} samples",
samples.size(),
getClass().getSimpleName().replace("Provider", "")
);
try {
final DaoSession session = getSession();
final GBDevice gbDevice = getDevice();
final Device device = DBHelper.findDevice(gbDevice, session);
if (device == null) {
LOG.warn("Device not found in database for '{}'", gbDevice.getAliasOrName());
return false;
}
final long deviceId = device.getId();
final User user = DBHelper.getUser(session);
final long userId = user.getId();
for (final T sample : samples) {
sample.setProvider(this);
sample.setDeviceId(deviceId);
sample.setUserId(userId);
}
addGBActivitySamples(samples);
} catch (final Exception e) {
LOG.error("Error saving samples", e);
final Context ctx = (context != null) ? context : GBApplication.getContext();
final String message = ctx.getString(R.string.persisting_samples_failed, e.getLocalizedMessage());
toast(ctx, message, Toast.LENGTH_LONG, GB.ERROR, e);
return false;
}
return true;
}
} }
@@ -90,7 +90,7 @@ public interface SampleProvider<T extends AbstractActivitySample> {
* timestamp will be overwritten. * timestamp will be overwritten.
* @param activitySamples the samples to add * @param activitySamples the samples to add
*/ */
void addGBActivitySamples(T[] activitySamples); void addGBActivitySamples(@NonNull List<T> activitySamples);
/** /**
* Factory method to creates an empty sample of the correct type for this sample provider * Factory method to creates an empty sample of the correct type for this sample provider
@@ -80,7 +80,7 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
} }
@Override @Override
public void addGBActivitySamples(AbstractActivitySample[] activitySamples) { public void addGBActivitySamples(@NonNull List<AbstractActivitySample> activitySamples) {
} }
@Override @Override
@@ -68,7 +68,7 @@ open class GenericHeartRateActivitySampleProvider(device: GBDevice, session: Dao
throw UnsupportedOperationException("Read-only sample provider") throw UnsupportedOperationException("Read-only sample provider")
} }
override fun addGBActivitySamples(activitySamples: Array<GenericActivitySample>) { override fun addGBActivitySamples(activitySamples: List<GenericActivitySample>) {
throw UnsupportedOperationException("Read-only sample provider") throw UnsupportedOperationException("Read-only sample provider")
} }
@@ -110,7 +110,7 @@ open class GloryFitActivitySampleProvider(device: GBDevice, session: DaoSession)
throw UnsupportedOperationException("Read-only sample provider") throw UnsupportedOperationException("Read-only sample provider")
} }
override fun addGBActivitySamples(activitySamples: Array<GenericActivitySample>) { override fun addGBActivitySamples(activitySamples: List<GenericActivitySample>) {
throw UnsupportedOperationException("Read-only sample provider") throw UnsupportedOperationException("Read-only sample provider")
} }
@@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -239,7 +240,7 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
} }
@Override @Override
public void addGBActivitySamples(HuaweiActivitySample[] activitySamples) { public void addGBActivitySamples(@NonNull List<HuaweiActivitySample> activitySamples) {
List<HuaweiActivitySample> newSamples = new ArrayList<>(); List<HuaweiActivitySample> newSamples = new ArrayList<>();
for (HuaweiActivitySample sample : activitySamples) { for (HuaweiActivitySample sample : activitySamples) {
HuaweiActivitySample start = copySample(sample); HuaweiActivitySample start = copySample(sample);
@@ -52,7 +52,7 @@ public class SonyWena3ActivitySampleCombiner {
alteredSamples.add(activitySample); alteredSamples.add(activitySample);
} }
} }
activitySampleProvider.addGBActivitySamples(alteredSamples.toArray(new Wena3ActivitySample[alteredSamples.size()])); activitySampleProvider.addGBActivitySamples(alteredSamples);
} }
public void overlayHeartRateStartingAt(Date startDate, SonyWena3HeartRateSampleProvider heartRateSampleProvider) { public void overlayHeartRateStartingAt(Date startDate, SonyWena3HeartRateSampleProvider heartRateSampleProvider) {
@@ -73,6 +73,6 @@ public class SonyWena3ActivitySampleCombiner {
alteredSamples.add(activitySample); alteredSamples.add(activitySample);
} }
} }
activitySampleProvider.addGBActivitySamples(alteredSamples.toArray(new Wena3ActivitySample[alteredSamples.size()])); activitySampleProvider.addGBActivitySamples(alteredSamples);
} }
} }
@@ -179,7 +179,7 @@ public class CmfActivitySync {
} }
LOG.debug("Will persist {} activity samples", samples.size()); LOG.debug("Will persist {} activity samples", samples.size());
sampleProvider.addGBActivitySamples(samples.toArray(new CmfActivitySample[0])); sampleProvider.addGBActivitySamples(samples);
} catch (final Exception e) { } catch (final Exception e) {
GB.toast(getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR, e); GB.toast(getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR, e);
} }
@@ -887,7 +887,7 @@ public class FitImporter {
sample.setUser(user); sample.setUser(user);
} }
sampleProvider.addGBActivitySamples(activitySamples.toArray(new GarminActivitySample[0])); sampleProvider.addGBActivitySamples(activitySamples);
} catch (final Exception e) { } catch (final Exception e) {
GB.toast(context, "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR, e); GB.toast(context, "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR, e);
} }
@@ -119,7 +119,7 @@ public class FetchActivityOperation extends AbstractRepeatingFetchOperation {
timestamp.add(Calendar.MINUTE, 1); timestamp.add(Calendar.MINUTE, 1);
} }
sampleProvider.addGBActivitySamples(samples.toArray(new MiBandActivitySample[0])); sampleProvider.addGBActivitySamples(samples);
timestamp.add(Calendar.MINUTE, -1); timestamp.add(Calendar.MINUTE, -1);
@@ -1881,7 +1881,7 @@ public class HuaweiSupportProvider {
samples.add(activitySample); samples.add(activitySample);
} }
sampleProvider.addGBActivitySamples(samples.toArray(new HuaweiActivitySample[0])); sampleProvider.addGBActivitySamples(samples);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Failed to add sleep activity to database", e); LOG.error("Failed to add sleep activity to database", e);
} }
@@ -121,7 +121,7 @@ public class GetStepDataRequest extends Request {
} }
} }
sampleProvider.addGBActivitySamples(samples.toArray(new HuaweiActivitySample[0])); sampleProvider.addGBActivitySamples(samples);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Failed to add step data to database", e); LOG.error("Failed to add step data to database", e);
} }
@@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
@@ -146,7 +147,7 @@ public class FetchActivityOperation extends AbstractID115Operation {
} }
ID115SampleProvider provider = new ID115SampleProvider(getDevice(), dbHandler.getDaoSession()); ID115SampleProvider provider = new ID115SampleProvider(getDevice(), dbHandler.getDaoSession());
provider.addGBActivitySamples(sampleArray); provider.addGBActivitySamples(Arrays.asList(sampleArray));
} catch (Exception ex) { } catch (Exception ex) {
GB.toast(getContext(), "Error saving activity data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); GB.toast(getContext(), "Error saving activity data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
} }
@@ -1638,7 +1638,7 @@ public class WatchXPlusDeviceSupport extends AbstractBTLESingleDeviceSupport {
overlayList.add(new WatchXPlusHealthActivityOverlay(sample.getTimestamp(), sample.getTimestamp()+300, sample.getRawKind(), sample.getDeviceId(), sample.getUserId(), sample.getRawWatchXPlusHealthData())); overlayList.add(new WatchXPlusHealthActivityOverlay(sample.getTimestamp(), sample.getTimestamp()+300, sample.getRawKind(), sample.getDeviceId(), sample.getUserId(), sample.getRawWatchXPlusHealthData()));
} }
overlayDao.insertOrReplaceInTx(overlayList); overlayDao.insertOrReplaceInTx(overlayList);
provider.addGBActivitySamples(samples.toArray(new WatchXPlusActivitySample[0])); provider.addGBActivitySamples(samples);
handleEndOfDataChunks(chunkNo, type); handleEndOfDataChunks(chunkNo, type);
} else if (DataType.HEART_RATE.equals(type)) { } else if (DataType.HEART_RATE.equals(type)) {
@@ -1658,7 +1658,7 @@ public class WatchXPlusDeviceSupport extends AbstractBTLESingleDeviceSupport {
sample.setRawKind(ActivityKind.ACTIVITY.getCode()); sample.setRawKind(ActivityKind.ACTIVITY.getCode());
samples.add(sample); samples.add(sample);
} }
provider.addGBActivitySamples(samples.toArray(new WatchXPlusActivitySample[0])); provider.addGBActivitySamples(samples);
handleEndOfDataChunks(chunkNo, type); handleEndOfDataChunks(chunkNo, type);
} else { } else {
@@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.UUID; import java.util.UUID;
@@ -389,7 +390,7 @@ public class FetchActivityOperation extends AbstractMiBand1Operation {
minutes++; minutes++;
timestampInSeconds += 60; timestampInSeconds += 60;
} }
provider.addGBActivitySamples(samples); provider.addGBActivitySamples(Arrays.asList(samples));
} finally { } finally {
activityStruct.bufferFlushed(minutes); activityStruct.bufferFlushed(minutes);
} }
@@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.SimpleTimeZone; import java.util.SimpleTimeZone;
import java.util.UUID; import java.util.UUID;
@@ -105,7 +106,7 @@ class AppMessageHandlerMisfit extends AppMessageHandler {
} }
LOG.info("total steps for above period: " + totalSteps); LOG.info("total steps for above period: " + totalSteps);
sampleProvider.addGBActivitySamples(misfitSamples); sampleProvider.addGBActivitySamples(Arrays.asList(misfitSamples));
} catch (Exception e) { } catch (Exception e) {
LOG.error("Error acquiring database", e); LOG.error("Error acquiring database", e);
return null; return null;
@@ -23,6 +23,7 @@ import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@@ -115,7 +116,7 @@ class DatalogSessionHealthSteps extends DatalogSessionPebbleHealth {
samples[j].setProvider(sampleProvider); samples[j].setProvider(sampleProvider);
} }
sampleProvider.addGBActivitySamples(samples); sampleProvider.addGBActivitySamples(Arrays.asList(samples));
} catch (Exception ex) { } catch (Exception ex) {
LOG.debug(ex.getMessage()); LOG.debug(ex.getMessage());
} }
@@ -15,6 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@@ -296,7 +297,7 @@ public class PolarH10DeviceSupport extends AbstractBTLESingleDeviceSupport {
final Long userId = DBHelper.getUser(db.getDaoSession()).getId(); final Long userId = DBHelper.getUser(db.getDaoSession()).getId();
final Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId(); final Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId();
final PolarH10ActivitySample sample = new PolarH10ActivitySample((int) (info.getTimestamp() / 1000), deviceId, userId, info.getHeartRate()); final PolarH10ActivitySample sample = new PolarH10ActivitySample((int) (info.getTimestamp() / 1000), deviceId, userId, info.getHeartRate());
polarSampleProvider.addGBActivitySamples(new PolarH10ActivitySample[]{sample}); polarSampleProvider.addGBActivitySamples(Collections.singletonList(sample));
final ArrayList<@NotNull Integer> rrIntervals = info.getRrIntervals(); final ArrayList<@NotNull Integer> rrIntervals = info.getRrIntervals();
if (!rrIntervals.isEmpty()) { if (!rrIntervals.isEmpty()) {
@@ -633,7 +633,7 @@ public class FossilWatchAdapter extends WatchAdapter {
LOG.debug("Activity samples parsed successfully: {}", Arrays.stream(samples).toList()); LOG.debug("Activity samples parsed successfully: {}", Arrays.stream(samples).toList());
} }
provider.addGBActivitySamples(samples); provider.addGBActivitySamples(Arrays.asList(samples));
if (saveRawActivityFiles) { if (saveRawActivityFiles) {
writeFile("activity_qhybrid", String.valueOf(System.currentTimeMillis()), fileData); writeFile("activity_qhybrid", String.valueOf(System.currentTimeMillis()), fileData);
@@ -80,6 +80,7 @@ import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@@ -1284,7 +1285,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
for (int i = 0; i < entries.size(); i++) { for (int i = 0; i < entries.size(); i++) {
samples[i] = entries.get(i).toDAOActivitySample(userId, deviceId); samples[i] = entries.get(i).toDAOActivitySample(userId, deviceId);
} }
provider.addGBActivitySamples(samples); provider.addGBActivitySamples(Arrays.asList(samples));
// SpO2, should be empty for an unsupported device // SpO2, should be empty for an unsupported device
ArrayList<HybridHRSpo2Sample> spo2Samples = parser.getSpo2Samples(); ArrayList<HybridHRSpo2Sample> spo2Samples = parser.getSpo2Samples();
HybridHRSpo2SampleProvider spo2Provider = new HybridHRSpo2SampleProvider(getDeviceSupport().getDevice(), dbHandler.getDaoSession()); HybridHRSpo2SampleProvider spo2Provider = new HybridHRSpo2SampleProvider(getDeviceSupport().getDevice(), dbHandler.getDaoSession());
@@ -60,7 +60,7 @@ public class StepsPacketParser extends OneBytePerSamplePacketParser {
i++; i++;
currentSampleDate = timestampOfSampleAtIndex(i); currentSampleDate = timestampOfSampleAtIndex(i);
} }
sampleProvider.addGBActivitySamples(samples.toArray(new Wena3ActivitySample[samples.size()])); sampleProvider.addGBActivitySamples(samples);
SonyWena3ActivitySampleCombiner combiner = new SonyWena3ActivitySampleCombiner(sampleProvider); SonyWena3ActivitySampleCombiner combiner = new SonyWena3ActivitySampleCombiner(sampleProvider);
@@ -220,7 +220,7 @@ public class ActivitySampleHandler extends AbstractResponseHandler {
sample.setDeviceId(deviceId); sample.setDeviceId(deviceId);
sample.setUserId(userId); sample.setUserId(userId);
} }
provider.addGBActivitySamples(activitySamples.toArray(new WithingsSteelHRActivitySample[0])); provider.addGBActivitySamples(activitySamples);
} catch (Exception ex) { } catch (Exception ex) {
logger.warn("Error saving activity data: " + ex.getLocalizedMessage()); logger.warn("Error saving activity data: " + ex.getLocalizedMessage());
} }
@@ -191,7 +191,7 @@ public class DailyDetailsParser extends XiaomiActivityParser {
sample.setUser(user); sample.setUser(user);
sample.setProvider(sampleProvider); sample.setProvider(sampleProvider);
} }
sampleProvider.addGBActivitySamples(samples.toArray(new XiaomiActivitySample[0])); sampleProvider.addGBActivitySamples(samples);
return true; return true;
} catch (final Exception e) { } catch (final Exception e) {