Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractTimeSampleProvider.java
MrYoranimo 508a86b8ed Xiaomi: fix determining fall asleep time
Because the previous implementation of determining the time the user
falls asleep in a given time range would take the 24 hours in advance
into account, graphs displaying sleep data would erroneously indicate
that the user has been asleep since the start of the timeframe if
the user was asleep during the rollover of the time frame 24 hours
before.

This commit change the algorithm to only fetch the last sleep stage
sample and sleep range sample from the database that occurred before
the given time range. This saves having to process 24 hours worth of
samples before the time range in both cases, and prevents taking into
account irrelevant sleep ranges.
2024-05-07 13:33:52 +02:00

159 lines
5.6 KiB
Java

/* Copyright (C) 2023-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;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractTimeSample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
/**
* Base class for all time sample providers. A Sample provider is device specific and provides
* access to the device specific samples. There are both read and write operations.
*
* @param <T> the sample type
*/
public abstract class AbstractTimeSampleProvider<T extends AbstractTimeSample> implements TimeSampleProvider<T> {
private final DaoSession mSession;
private final GBDevice mDevice;
protected AbstractTimeSampleProvider(final GBDevice device, final DaoSession session) {
mDevice = device;
mSession = session;
}
public GBDevice getDevice() {
return mDevice;
}
public DaoSession getSession() {
return mSession;
}
@NonNull
@Override
public List<T> getAllSamples(final long timestampFrom, final long timestampTo) {
final QueryBuilder<T> qb = getSampleDao().queryBuilder();
final Property timestampProperty = getTimestampSampleProperty();
final Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
// no device, no samples
return Collections.emptyList();
}
final Property deviceProperty = getDeviceIdentifierSampleProperty();
qb.where(deviceProperty.eq(dbDevice.getId()), timestampProperty.ge(timestampFrom))
.where(timestampProperty.le(timestampTo));
final List<T> samples = qb.build().list();
detachFromSession();
return samples;
}
@Override
public void addSample(final T activitySample) {
getSampleDao().insertOrReplace(activitySample);
}
@Override
public void addSamples(final List<T> activitySamples) {
getSampleDao().insertOrReplaceInTx(activitySamples);
}
@Nullable
@Override
public T getLatestSample() {
final QueryBuilder<T> qb = getSampleDao().queryBuilder();
final Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
// no device, no sample
return null;
}
final Property deviceProperty = getDeviceIdentifierSampleProperty();
qb.where(deviceProperty.eq(dbDevice.getId())).orderDesc(getTimestampSampleProperty()).limit(1);
final List<T> samples = qb.build().list();
if (samples.isEmpty()) {
return null;
}
return samples.get(0);
}
public T getLastSampleBefore(final long timestampTo) {
final Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
// no device, no sample
return null;
}
final Property deviceIdSampleProp = getDeviceIdentifierSampleProperty();
final Property timestampSampleProp = getTimestampSampleProperty();
final List<T> samples = getSampleDao().queryBuilder()
.where(deviceIdSampleProp.eq(dbDevice.getId()),
timestampSampleProp.le(timestampTo))
.orderDesc(getTimestampSampleProperty())
.limit(1)
.list();
return !samples.isEmpty() ? samples.get(0) : null;
}
@Nullable
@Override
public T getFirstSample() {
final QueryBuilder<T> qb = getSampleDao().queryBuilder();
final Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
// no device, no sample
return null;
}
final Property deviceProperty = getDeviceIdentifierSampleProperty();
qb.where(deviceProperty.eq(dbDevice.getId())).orderAsc(getTimestampSampleProperty()).limit(1);
final List<T> samples = qb.build().list();
if (samples.isEmpty()) {
return null;
}
return samples.get(0);
}
/**
* Detaches all samples of this type from the session. Changes to them may not be
* written back to the database.
* <p>
* Subclasses should call this method after performing custom queries.
*/
protected void detachFromSession() {
getSampleDao().detachAll();
}
@NonNull
public abstract AbstractDao<T, ?> getSampleDao();
@NonNull
protected abstract Property getTimestampSampleProperty();
@NonNull
protected abstract Property getDeviceIdentifierSampleProperty();
}