From 09865f3943fb7aee9f219b405a76d9159cec7cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Tue, 20 Aug 2024 15:34:48 +0100 Subject: [PATCH] Garmin: Store pending files for processing in the database --- .../gadgetbridge/daogen/GBDaoGenerator.java | 25 ++++- .../devices/PendingFileProvider.java | 100 ++++++++++++++++++ .../devices/garmin/GarminCoordinator.java | 5 + .../service/devices/garmin/GarminSupport.java | 45 +++++--- .../devices/garmin/fit/FitAsyncProcessor.java | 18 ++++ 5 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/PendingFileProvider.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index b8cf1ac5a..fb821a5d8 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -45,7 +45,7 @@ public class GBDaoGenerator { 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 user = addUserInfo(schema, userAttributes); @@ -116,6 +116,7 @@ public class GBDaoGenerator { addGarminEventSample(schema, user, device); addGarminHrvSummarySample(schema, user, device); addGarminHrvValueSample(schema, user, device); + addPendingFile(schema, user, device); addWena3EnergySample(schema, user, device); addWena3BehaviorSample(schema, user, device); addWena3CaloriesSample(schema, user, device); @@ -755,6 +756,28 @@ public class GBDaoGenerator { 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) { Entity activitySample = addEntity(schema, "WatchXPlusActivitySample"); activitySample.implementsSerializable(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/PendingFileProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/PendingFileProvider.java new file mode 100644 index 000000000..729911e32 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/PendingFileProvider.java @@ -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 . */ +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 getAllPendingFiles() { + final QueryBuilder 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 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 qb = pendingFileDao.queryBuilder(); + qb.where(PendingFileDao.Properties.DeviceId.eq(device.getId())); + qb.where(PendingFileDao.Properties.Path.eq(path)); + final List pendingFiles = qb.build().list(); + if (!pendingFiles.isEmpty()) { + return pendingFiles.get(0); + } + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java index e54abe991..383b323fc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java @@ -27,6 +27,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2SampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.PendingFileDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample; @@ -71,6 +72,10 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { session.getBaseActivitySummaryDao().queryBuilder() .where(BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId)) .buildDelete().executeDeleteWithoutDetachingEntities(); + + session.getPendingFileDao().queryBuilder() + .where(PendingFileDao.Properties.DeviceId.eq(deviceId)) + .buildDelete().executeDeleteWithoutDetachingEntities(); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java index 78358ca92..aac49b5f7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java @@ -29,19 +29,18 @@ import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; +import java.util.stream.Collectors; -import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; -import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; 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.GarminGpxRouteInstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; -import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; @@ -97,7 +96,6 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni private final Queue filesToDownload; private final List messageHandlers; private final List supportedFileTypeList = new ArrayList<>(); - private final List filesToProcess = new ArrayList<>(); private ICommunicator communicator; private MusicStateSpec musicStateSpec; private Timer musicStateTimer; @@ -298,7 +296,15 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni LOG.debug("FILE DOWNLOAD COMPLETE {}", filename); 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 @@ -309,6 +315,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni super.evaluateGBDeviceEvent(deviceEvent); } + /** @noinspection BooleanMethodIsAlwaysInverted*/ private boolean getKeepActivityDataOnDevice() { 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++) { if (day < weather.forecasts.size()) { + //noinspection ExtractMethodRecommender WeatherSpec.Daily daily = weather.forecasts.get(day); int ts = weather.timestamp + (day + 1) * 24 * 60 * 60; RecordData weatherDailyForecast = new RecordData(recordDefinitionDaily, recordDefinitionDaily.getRecordHeader()); @@ -477,6 +485,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni return; } + //noinspection SwitchStatementWithTooFewBranches switch (config) { case PREF_SEND_APP_NOTIFICATIONS: NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent(); @@ -484,8 +493,6 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni evaluateGBDeviceEvent(notificationSubscriptionDeviceEvent); return; } - - } private void processDownloadQueue() { @@ -515,7 +522,23 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni } if (filesToDownload.isEmpty() && !fileTransferHandler.isDownloading() && isBusyFetching) { + final List 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()) { + LOG.debug("No pending files to process"); // No downloaded fit files to process if (gbDevice.isBusy() && isBusyFetching) { 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 final FitAsyncProcessor fitAsyncProcessor = new FitAsyncProcessor(getContext(), getDevice()); - final List filesToProcessClone = new ArrayList<>(filesToProcess); - filesToProcess.clear(); final long[] lastNotificationUpdateTs = new long[]{System.currentTimeMillis()}; - fitAsyncProcessor.process(filesToProcessClone, new FitAsyncProcessor.Callback() { + fitAsyncProcessor.process(filesToProcess, new FitAsyncProcessor.Callback() { @Override public void onProgress(final int i) { final long now = System.currentTimeMillis(); if (now - lastNotificationUpdateTs[0] > 1500L) { lastNotificationUpdateTs[0] = now; GB.updateTransferNotification( - "Parsing fit files", "File " + i + " of " + filesToProcessClone.size(), + "Parsing fit files", "File " + i + " of " + filesToProcess.size(), true, - (i * 100) / filesToProcessClone.size(), getContext() + (i * 100) / filesToProcess.size(), getContext() ); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitAsyncProcessor.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitAsyncProcessor.java index 477d1f691..a29425dbd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitAsyncProcessor.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitAsyncProcessor.java @@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; import android.content.Context; import android.os.Handler; +import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,7 +10,13 @@ import org.slf4j.LoggerFactory; import java.io.File; 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.service.devices.garmin.deviceevents.FileDownloadedDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.util.GB; public class FitAsyncProcessor { private static final Logger LOG = LoggerFactory.getLogger(FitAsyncProcessor.class); @@ -45,6 +52,17 @@ public class FitAsyncProcessor { fitImporter.importFile(file); } catch (final Exception 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) {