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) {