1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-09 03:37:03 +01:00

Garmin: Store pending files for processing in the database

This commit is contained in:
José Rebelo 2024-08-20 15:34:48 +01:00
parent d0b525f420
commit 09865f3943
5 changed files with 180 additions and 13 deletions

View File

@ -45,7 +45,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
final Schema schema = new Schema(76, MAIN_PACKAGE + ".entities"); final Schema schema = new Schema(77, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema); Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes); Entity user = addUserInfo(schema, userAttributes);
@ -116,6 +116,7 @@ public class GBDaoGenerator {
addGarminEventSample(schema, user, device); addGarminEventSample(schema, user, device);
addGarminHrvSummarySample(schema, user, device); addGarminHrvSummarySample(schema, user, device);
addGarminHrvValueSample(schema, user, device); addGarminHrvValueSample(schema, user, device);
addPendingFile(schema, user, device);
addWena3EnergySample(schema, user, device); addWena3EnergySample(schema, user, device);
addWena3BehaviorSample(schema, user, device); addWena3BehaviorSample(schema, user, device);
addWena3CaloriesSample(schema, user, device); addWena3CaloriesSample(schema, user, device);
@ -755,6 +756,28 @@ public class GBDaoGenerator {
return hrvValueSample; return hrvValueSample;
} }
private static Entity addPendingFile(Schema schema, Entity user, Entity device) {
Entity pendingFile = addEntity(schema, "PendingFile");
pendingFile.setJavaDoc(
"This class represents a file that was fetched from the device and is pending processing."
);
// We need a single-column primary key so that we can delete records
pendingFile.addIdProperty().autoincrement();
Property path = pendingFile.addStringProperty("path").notNull().getProperty();
Property deviceId = pendingFile.addLongProperty("deviceId").notNull().getProperty();
pendingFile.addToOne(device, deviceId);
final Index indexUnique = new Index();
indexUnique.addProperty(deviceId);
indexUnique.addProperty(path);
indexUnique.makeUnique();
pendingFile.addIndex(indexUnique);
return pendingFile;
}
private static Entity addWatchXPlusHealthActivitySample(Schema schema, Entity user, Entity device) { private static Entity addWatchXPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "WatchXPlusActivitySample"); Entity activitySample = addEntity(schema, "WatchXPlusActivitySample");
activitySample.implementsSerializable(); activitySample.implementsSerializable();

View File

@ -0,0 +1,100 @@
/* 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;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.PendingFile;
import nodomain.freeyourgadget.gadgetbridge.entities.PendingFileDao;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public final class PendingFileProvider {
private final DaoSession mSession;
private final GBDevice mDevice;
public PendingFileProvider(final GBDevice device, final DaoSession session) {
mDevice = device;
mSession = session;
}
@NonNull
public List<PendingFile> getAllPendingFiles() {
final QueryBuilder<PendingFile> qb = mSession.getPendingFileDao().queryBuilder();
final Device dbDevice = DBHelper.findDevice(mDevice, mSession);
if (dbDevice == null) {
// no device, no pending files
return Collections.emptyList();
}
final Property deviceProperty = PendingFileDao.Properties.DeviceId;
qb.where(deviceProperty.eq(dbDevice.getId()));
final List<PendingFile> ret = qb.build().list();
mSession.getPendingFileDao().detachAll();
return ret;
}
public void removePendingFile(final String path) {
final PendingFile pendingFile = findByPath(path);
if (pendingFile != null) {
pendingFile.delete();
}
}
public void addPendingFile(final String path) {
final PendingFile existingFile = findByPath(path);
if (existingFile != null) {
return;
}
final Device device = DBHelper.getDevice(mDevice, mSession);
final PendingFile pendingFile = new PendingFile();
pendingFile.setPath(path);
pendingFile.setDevice(device);
addPendingFile(pendingFile);
}
public void addPendingFile(final PendingFile pendingFile) {
mSession.getPendingFileDao().insertOrReplace(pendingFile);
}
@Nullable
private PendingFile findByPath(final String path) {
final Device device = DBHelper.getDevice(mDevice, mSession);
final PendingFileDao pendingFileDao = mSession.getPendingFileDao();
final QueryBuilder<PendingFile> qb = pendingFileDao.queryBuilder();
qb.where(PendingFileDao.Properties.DeviceId.eq(device.getId()));
qb.where(PendingFileDao.Properties.Path.eq(path));
final List<PendingFile> pendingFiles = qb.build().list();
if (!pendingFiles.isEmpty()) {
return pendingFiles.get(0);
}
return null;
}
}

View File

@ -27,6 +27,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2SampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.PendingFileDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample;
@ -71,6 +72,10 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
session.getBaseActivitySummaryDao().queryBuilder() session.getBaseActivitySummaryDao().queryBuilder()
.where(BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId)) .where(BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId))
.buildDelete().executeDeleteWithoutDetachingEntities(); .buildDelete().executeDeleteWithoutDetachingEntities();
session.getPendingFileDao().queryBuilder()
.where(PendingFileDao.Properties.DeviceId.eq(deviceId))
.buildDelete().executeDeleteWithoutDetachingEntities();
} }
@Override @Override

View File

@ -29,19 +29,18 @@ import java.util.Set;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.PendingFileProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminGpxRouteInstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminGpxRouteInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability; import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService; import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
@ -97,7 +96,6 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
private final Queue<FileTransferHandler.DirectoryEntry> filesToDownload; private final Queue<FileTransferHandler.DirectoryEntry> filesToDownload;
private final List<MessageHandler> messageHandlers; private final List<MessageHandler> messageHandlers;
private final List<FileType> supportedFileTypeList = new ArrayList<>(); private final List<FileType> supportedFileTypeList = new ArrayList<>();
private final List<File> filesToProcess = new ArrayList<>();
private ICommunicator communicator; private ICommunicator communicator;
private MusicStateSpec musicStateSpec; private MusicStateSpec musicStateSpec;
private Timer musicStateTimer; private Timer musicStateTimer;
@ -298,7 +296,15 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
LOG.debug("FILE DOWNLOAD COMPLETE {}", filename); LOG.debug("FILE DOWNLOAD COMPLETE {}", filename);
if (entry.getFiletype().isFitFile()) { if (entry.getFiletype().isFitFile()) {
filesToProcess.add(new File(((FileDownloadedDeviceEvent) deviceEvent).localPath)); try (DBHandler handler = GBApplication.acquireDB()) {
final DaoSession session = handler.getDaoSession();
final PendingFileProvider pendingFileProvider = new PendingFileProvider(gbDevice, session);
pendingFileProvider.addPendingFile(((FileDownloadedDeviceEvent) deviceEvent).localPath);
} catch (final Exception e) {
GB.toast(getContext(), "Error saving pending file", Toast.LENGTH_LONG, GB.ERROR, e);
}
} }
if (!getKeepActivityDataOnDevice()) { // delete file from watch upon successful download if (!getKeepActivityDataOnDevice()) { // delete file from watch upon successful download
@ -309,6 +315,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
super.evaluateGBDeviceEvent(deviceEvent); super.evaluateGBDeviceEvent(deviceEvent);
} }
/** @noinspection BooleanMethodIsAlwaysInverted*/
private boolean getKeepActivityDataOnDevice() { private boolean getKeepActivityDataOnDevice() {
return getDevicePrefs().getBoolean("keep_activity_data_on_device", false); return getDevicePrefs().getBoolean("keep_activity_data_on_device", false);
} }
@ -423,6 +430,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
for (int day = 0; day < 4; day++) { for (int day = 0; day < 4; day++) {
if (day < weather.forecasts.size()) { if (day < weather.forecasts.size()) {
//noinspection ExtractMethodRecommender
WeatherSpec.Daily daily = weather.forecasts.get(day); WeatherSpec.Daily daily = weather.forecasts.get(day);
int ts = weather.timestamp + (day + 1) * 24 * 60 * 60; int ts = weather.timestamp + (day + 1) * 24 * 60 * 60;
RecordData weatherDailyForecast = new RecordData(recordDefinitionDaily, recordDefinitionDaily.getRecordHeader()); RecordData weatherDailyForecast = new RecordData(recordDefinitionDaily, recordDefinitionDaily.getRecordHeader());
@ -477,6 +485,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
return; return;
} }
//noinspection SwitchStatementWithTooFewBranches
switch (config) { switch (config) {
case PREF_SEND_APP_NOTIFICATIONS: case PREF_SEND_APP_NOTIFICATIONS:
NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent(); NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent();
@ -484,8 +493,6 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
evaluateGBDeviceEvent(notificationSubscriptionDeviceEvent); evaluateGBDeviceEvent(notificationSubscriptionDeviceEvent);
return; return;
} }
} }
private void processDownloadQueue() { private void processDownloadQueue() {
@ -515,7 +522,23 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
} }
if (filesToDownload.isEmpty() && !fileTransferHandler.isDownloading() && isBusyFetching) { if (filesToDownload.isEmpty() && !fileTransferHandler.isDownloading() && isBusyFetching) {
final List<File> filesToProcess;
try (DBHandler handler = GBApplication.acquireDB()) {
final DaoSession session = handler.getDaoSession();
final PendingFileProvider pendingFileProvider = new PendingFileProvider(gbDevice, session);
filesToProcess = pendingFileProvider.getAllPendingFiles()
.stream()
.map(pf -> new File(pf.getPath()))
.collect(Collectors.toList());
} catch (final Exception e) {
LOG.error("Failed to get pending files", e);
return;
}
if (filesToProcess.isEmpty()) { if (filesToProcess.isEmpty()) {
LOG.debug("No pending files to process");
// No downloaded fit files to process // No downloaded fit files to process
if (gbDevice.isBusy() && isBusyFetching) { if (gbDevice.isBusy() && isBusyFetching) {
GB.signalActivityDataFinish(); GB.signalActivityDataFinish();
@ -530,19 +553,17 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
// Keep the device marked as busy while we process the files asynchronously // Keep the device marked as busy while we process the files asynchronously
final FitAsyncProcessor fitAsyncProcessor = new FitAsyncProcessor(getContext(), getDevice()); final FitAsyncProcessor fitAsyncProcessor = new FitAsyncProcessor(getContext(), getDevice());
final List<File> filesToProcessClone = new ArrayList<>(filesToProcess);
filesToProcess.clear();
final long[] lastNotificationUpdateTs = new long[]{System.currentTimeMillis()}; final long[] lastNotificationUpdateTs = new long[]{System.currentTimeMillis()};
fitAsyncProcessor.process(filesToProcessClone, new FitAsyncProcessor.Callback() { fitAsyncProcessor.process(filesToProcess, new FitAsyncProcessor.Callback() {
@Override @Override
public void onProgress(final int i) { public void onProgress(final int i) {
final long now = System.currentTimeMillis(); final long now = System.currentTimeMillis();
if (now - lastNotificationUpdateTs[0] > 1500L) { if (now - lastNotificationUpdateTs[0] > 1500L) {
lastNotificationUpdateTs[0] = now; lastNotificationUpdateTs[0] = now;
GB.updateTransferNotification( GB.updateTransferNotification(
"Parsing fit files", "File " + i + " of " + filesToProcessClone.size(), "Parsing fit files", "File " + i + " of " + filesToProcess.size(),
true, true,
(i * 100) / filesToProcessClone.size(), getContext() (i * 100) / filesToProcess.size(), getContext()
); );
} }
} }

View File

@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.widget.Toast;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -9,7 +10,13 @@ import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.PendingFileProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.FileDownloadedDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FitAsyncProcessor { public class FitAsyncProcessor {
private static final Logger LOG = LoggerFactory.getLogger(FitAsyncProcessor.class); private static final Logger LOG = LoggerFactory.getLogger(FitAsyncProcessor.class);
@ -45,6 +52,17 @@ public class FitAsyncProcessor {
fitImporter.importFile(file); fitImporter.importFile(file);
} catch (final Exception ex) { } catch (final Exception ex) {
LOG.error("Exception while importing {}", file, ex); LOG.error("Exception while importing {}", file, ex);
continue; // do not remove from pending files
}
try (DBHandler handler = GBApplication.acquireDB()) {
final DaoSession session = handler.getDaoSession();
final PendingFileProvider pendingFileProvider = new PendingFileProvider(gbDevice, session);
pendingFileProvider.removePendingFile(file.getPath());
} catch (final Exception e) {
LOG.error("Exception while removing pending file {}", file, e);
} }
} }
} catch (final Exception e) { } catch (final Exception e) {