mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Merge pull request 'Health Connect: Harden sync-start selection against non-positive sample timestamps' (#6207)
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6207
This commit is contained in:
+23
@@ -182,6 +182,29 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
|
||||
return sample;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public T getFirstActivitySample(final int after) {
|
||||
QueryBuilder<T> qb = getSampleDao().queryBuilder();
|
||||
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
|
||||
if (dbDevice == null) {
|
||||
// no device, no sample
|
||||
return null;
|
||||
}
|
||||
Property deviceProperty = getDeviceIdentifierSampleProperty();
|
||||
Property timestampProperty = getTimestampSampleProperty();
|
||||
qb.where(timestampProperty.gt(after))
|
||||
.where(deviceProperty.eq(dbDevice.getId()))
|
||||
.orderAsc(timestampProperty).limit(1);
|
||||
List<T> samples = qb.build().list();
|
||||
if (samples.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
T sample = samples.get(0);
|
||||
sample.setProvider(this);
|
||||
return sample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the activity samples between two timestamps (inclusive). Exactly one every minute.
|
||||
* @param timestamp_from Start timestamp
|
||||
|
||||
@@ -119,4 +119,11 @@ public interface SampleProvider<T extends AbstractActivitySample> {
|
||||
@Nullable
|
||||
T getFirstActivitySample();
|
||||
|
||||
/**
|
||||
* Returns the activity sample with the oldest timestamp strictly after the given limit, or null if none
|
||||
* @return the oldest sample after the limit or null
|
||||
*/
|
||||
@Nullable
|
||||
T getFirstActivitySample(int after);
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -106,6 +106,12 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public AbstractActivitySample getFirstActivitySample(final int after) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public UnknownDeviceCoordinator() {
|
||||
|
||||
+17
@@ -16,11 +16,13 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.generic_hr
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.GenericHeartRateSampleProvider
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GenericActivitySample
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GenericHeartRateSample
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GenericHeartRateSampleDao
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind
|
||||
|
||||
@@ -88,6 +90,21 @@ open class GenericHeartRateActivitySampleProvider(device: GBDevice, session: Dao
|
||||
return heartRateProvider.firstSample?.let { hrSample -> toGenericActivitySample(hrSample) }
|
||||
}
|
||||
|
||||
override fun getFirstActivitySample(after: Int): GenericActivitySample? {
|
||||
val afterMillis = after.toLong() * 1000
|
||||
val device = DBHelper.findDevice(heartRateProvider.device, heartRateProvider.session) ?: return null
|
||||
val samples = heartRateProvider.sampleDao.queryBuilder()
|
||||
.where(
|
||||
GenericHeartRateSampleDao.Properties.Timestamp.gt(afterMillis),
|
||||
GenericHeartRateSampleDao.Properties.DeviceId.eq(device.id)
|
||||
)
|
||||
.orderAsc(GenericHeartRateSampleDao.Properties.Timestamp)
|
||||
.limit(1)
|
||||
.build()
|
||||
.list()
|
||||
return samples.firstOrNull()?.let { toGenericActivitySample(it) }
|
||||
}
|
||||
|
||||
private fun toGenericActivitySample(hrSample: GenericHeartRateSample): GenericActivitySample {
|
||||
val activitySample = GenericActivitySample()
|
||||
activitySample.provider = this
|
||||
|
||||
+6
@@ -137,6 +137,12 @@ open class GloryFitActivitySampleProvider(device: GBDevice, session: DaoSession)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getFirstActivitySample(after: Int): GenericActivitySample? {
|
||||
// TODO getFirstActivitySample
|
||||
LOG.warn("getFirstActivitySample(after) not implemented");
|
||||
return null
|
||||
}
|
||||
|
||||
fun overlaySleep(samples: MutableList<GenericActivitySample>, timestampFrom: Int, timestampTo: Int) {
|
||||
val stagesMap = RangeMap<Long, ActivityKind?>(RangeMap.Mode.LOWER_BOUND)
|
||||
|
||||
|
||||
+7
-2
@@ -497,6 +497,11 @@ class HealthConnectUtils {
|
||||
private const val INITIAL_DELAY_MS = 1000L
|
||||
internal const val HC_SYNC_TAG = "[HC_SYNC]"
|
||||
|
||||
// Floor the sync-start at 2015 (Gadgetbridge predates it). A bogus near-epoch sample
|
||||
// timestamp otherwise resolves the start to ~1970 and triggers a full historical resync.
|
||||
private const val MIN_VALID_SAMPLE_SECONDS = 1420070400L // 2015-01-01T00:00:00Z
|
||||
private const val MIN_VALID_SAMPLE_MILLIS = MIN_VALID_SAMPLE_SECONDS * 1000
|
||||
|
||||
private fun getSyncTimestampRange(
|
||||
context: Context,
|
||||
gbDevice: GBDevice,
|
||||
@@ -683,10 +688,10 @@ class HealthConnectUtils {
|
||||
): Instant? {
|
||||
return when (val provider = getProviderForDataType(deviceCoordinator, device, db, dataType)) {
|
||||
is TimeSampleProvider<*> -> {
|
||||
provider.firstSample?.timestamp?.takeIf { it > 0 }?.let { Instant.ofEpochMilli(it) }
|
||||
provider.firstSample?.timestamp?.takeIf { it > MIN_VALID_SAMPLE_MILLIS }?.let { Instant.ofEpochMilli(it) }
|
||||
}
|
||||
is SampleProvider<*> -> { // For ActivitySample based providers
|
||||
provider.firstActivitySample?.timestamp?.takeIf { it > 0 }?.let { Instant.ofEpochSecond(it.toLong()) }
|
||||
provider.getFirstActivitySample(MIN_VALID_SAMPLE_SECONDS.toInt())?.timestamp?.takeIf { it > MIN_VALID_SAMPLE_SECONDS }?.let { Instant.ofEpochSecond(it.toLong()) }
|
||||
}
|
||||
is BaseActivitySummaryDao -> {
|
||||
val deviceEntity = DBHelper.getDevice(device, db.daoSession) ?: return null
|
||||
|
||||
+26
@@ -19,6 +19,7 @@ import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -170,6 +171,31 @@ public class SampleProviderTest extends TestBase {
|
||||
// FIXME assertEquals(1, activitySamples.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFirstActivitySampleSkipsNonPositiveTimestamp() {
|
||||
MiBandSampleProvider sampleProvider = new MiBandSampleProvider(dummyGBDevice, daoSession);
|
||||
User user = DBHelper.getUser(daoSession);
|
||||
Device device = DBHelper.getDevice(dummyGBDevice, daoSession);
|
||||
|
||||
MiBandActivitySample zero = createSample(sampleProvider, MiBandSampleProvider.TYPE_ACTIVITY, 0, 10, 70, 1000, user, device);
|
||||
MiBandActivitySample valid = createSample(sampleProvider, MiBandSampleProvider.TYPE_ACTIVITY, 100, 20, 80, 1030, user, device);
|
||||
sampleProvider.addGBActivitySamples(new MiBandActivitySample[] { zero, valid });
|
||||
|
||||
// oldest row overall is the timestamp 0 row
|
||||
assertEquals(0, sampleProvider.getFirstActivitySample().getTimestamp());
|
||||
|
||||
// but the overload skips it and returns the first row with timestamp > 0
|
||||
MiBandActivitySample first = sampleProvider.getFirstActivitySample(0);
|
||||
assertNotNull(first);
|
||||
assertEquals(100, first.getTimestamp());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFirstActivitySampleNoSamples() {
|
||||
MiBandSampleProvider sampleProvider = new MiBandSampleProvider(dummyGBDevice, daoSession);
|
||||
assertNull(sampleProvider.getFirstActivitySample(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHighResSamples() {
|
||||
// Mi Band sample provider does not support this at the moment, so we use the Huawei sample provider
|
||||
|
||||
Reference in New Issue
Block a user