diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java index 5098ff7a1..bd5778637 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java @@ -134,18 +134,20 @@ public class FileTransferHandler implements MessageHandler { private void saveFileToExternalStorage() { File dir; + File outputFile; try { dir = deviceSupport.getWritableExportDirectory(); - File outputFile = new File(dir, currentlyDownloading.getFileName()); + outputFile = new File(dir, currentlyDownloading.getFileName()); FileUtils.copyStreamToFile(new ByteArrayInputStream(currentlyDownloading.dataHolder.array()), outputFile); outputFile.setLastModified(currentlyDownloading.directoryEntry.fileDate.getTime()); - - } catch (IOException e) { + } catch (final IOException e) { LOG.error("Failed to save file", e); + return; // do not signal file as saved } FileDownloadedDeviceEvent fileDownloadedDeviceEvent = new FileDownloadedDeviceEvent(); fileDownloadedDeviceEvent.directoryEntry = currentlyDownloading.directoryEntry; + fileDownloadedDeviceEvent.localPath = outputFile.getAbsolutePath(); deviceSupport.evaluateGBDeviceEvent(fileDownloadedDeviceEvent); } 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 93bb44c71..d65e086ef 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 @@ -14,13 +14,13 @@ import java.io.FileOutputStream; import java.io.IOException; import java.text.DecimalFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Queue; import java.util.Timer; import java.util.TimerTask; @@ -65,7 +65,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents. import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitImporter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitAsyncProcessor; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.PredefinedLocalMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; @@ -98,7 +98,9 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni private ICommunicator communicator; private MusicStateSpec musicStateSpec; private Timer musicStateTimer; + private final List supportedFileTypeList = new ArrayList<>(); + private final List filesToProcess = new ArrayList<>(); public GarminSupport() { super(LOG); @@ -277,14 +279,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni LOG.debug("FILE DOWNLOAD COMPLETE {}", filename); if (entry.getFiletype().isFitFile()) { - try { - final File dir = getWritableExportDirectory(); - final File file = new File(dir, filename); - final FitImporter fitImporter = new FitImporter(getContext(), getDevice()); - fitImporter.importFile(file); - } catch (final IOException e) { - LOG.error("Failed to import fit file", e); - } + filesToProcess.add(new File(((FileDownloadedDeviceEvent) deviceEvent).localPath)); } if (!getKeepActivityDataOnDevice()) { // delete file from watch upon successful download @@ -473,47 +468,77 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni } } + private boolean isBusyFetching; + private void processDownloadQueue() { moveFilesFromLegacyCache(); //TODO: remove before merging if (!filesToDownload.isEmpty() && !fileTransferHandler.isDownloading()) { if (!gbDevice.isBusy()) { + isBusyFetching = true; GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data), "", true, 0, getContext()); getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data)); getDevice().sendDeviceUpdateIntent(getContext()); } - try { - FileTransferHandler.DirectoryEntry directoryEntry = filesToDownload.remove(); - while (checkFileExists(directoryEntry.getFileName()) || checkFileExists(directoryEntry.getLegacyFileName())) { + while (!filesToDownload.isEmpty()) { + final FileTransferHandler.DirectoryEntry directoryEntry = filesToDownload.remove(); + if (checkFileExists(directoryEntry.getFileName()) || checkFileExists(directoryEntry.getLegacyFileName())) { LOG.debug("File: {} already downloaded, not downloading again.", directoryEntry.getFileName()); if (!getKeepActivityDataOnDevice()) { // delete file from watch if already downloaded sendOutgoingMessage(new SetFileFlagsMessage(directoryEntry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE)); } - directoryEntry = filesToDownload.remove(); + continue; } - DownloadRequestMessage downloadRequestMessage = fileTransferHandler.downloadDirectoryEntry(directoryEntry); + + final DownloadRequestMessage downloadRequestMessage = fileTransferHandler.downloadDirectoryEntry(directoryEntry); if (downloadRequestMessage != null) { sendOutgoingMessage(downloadRequestMessage); + return; } else { LOG.debug("File: {} already downloaded, not downloading again, from inside.", directoryEntry.getFileName()); } - } catch (NoSuchElementException e) { - // we ran out of files to download - // FIXME this is ugly - if (gbDevice.isBusy() && gbDevice.getBusyTask().equals(getContext().getString(R.string.busy_task_fetch_activity_data))) { + } + } + + if (filesToDownload.isEmpty() && !fileTransferHandler.isDownloading() && isBusyFetching) { + if (filesToProcess.isEmpty()) { + // No downloaded fit files to process + if (gbDevice.isBusy() && isBusyFetching) { + GB.signalActivityDataFinish(); getDevice().unsetBusyTask(); GB.updateTransferNotification(null, "", false, 100, getContext()); getDevice().sendDeviceUpdateIntent(getContext()); } + isBusyFetching = false; + return; } - } else if (filesToDownload.isEmpty() && !fileTransferHandler.isDownloading()) { - if (gbDevice.isBusy() && gbDevice.getBusyTask().equals(getContext().getString(R.string.busy_task_fetch_activity_data))) { - getDevice().unsetBusyTask(); - GB.updateTransferNotification(null, "", false, 100, getContext()); - getDevice().sendDeviceUpdateIntent(getContext()); - } + + // 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(); + fitAsyncProcessor.process(filesToProcessClone, new FitAsyncProcessor.Callback() { + @Override + public void onProgress(final int i) { + GB.updateTransferNotification( + "Parsing fit files", "File " + i + " of " + filesToProcessClone.size(), + true, + (i * 100) / filesToProcessClone.size(), getContext() + ); + } + + @Override + public void onFinish() { + GB.signalActivityDataFinish(); + getDevice().unsetBusyTask(); + GB.updateTransferNotification(null, "", false, 100, getContext()); + getDevice().sendDeviceUpdateIntent(getContext()); + isBusyFetching = false; + } + }); } } @@ -766,25 +791,22 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni GB.toast(getContext(), "Error deleting activity data", Toast.LENGTH_LONG, GB.ERROR, e); } - try { - int i = 0; - for (final File file : fitFiles) { - i++; - LOG.debug("Parsing {}", file); - - GB.updateTransferNotification("Parsing fit files", "File " + i + " of " + fitFiles.length, true, (i * 100) / fitFiles.length, getContext()); - - try { - final FitImporter fitImporter = new FitImporter(getContext(), getDevice()); - fitImporter.importFile(file); - } catch (final Exception ex) { - LOG.error("Exception while importing {}", file, ex); - } + final FitAsyncProcessor fitAsyncProcessor = new FitAsyncProcessor(getContext(), getDevice()); + fitAsyncProcessor.process(Arrays.asList(fitFiles), new FitAsyncProcessor.Callback() { + @Override + public void onProgress(final int i) { + GB.updateTransferNotification( + "Parsing fit files", "File " + i + " of " + fitFiles.length, + true, + (i * 100) / fitFiles.length, getContext() + ); } - } catch (final Exception e) { - LOG.error("Failed to parse from storage", e); - } - GB.updateTransferNotification("", "", false, 100, getContext()); + @Override + public void onFinish() { + GB.updateTransferNotification("", "", false, 100, getContext()); + GB.signalActivityDataFinish(); + } + }); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/FileDownloadedDeviceEvent.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/FileDownloadedDeviceEvent.java index 0c56727a6..fb68a1429 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/FileDownloadedDeviceEvent.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/FileDownloadedDeviceEvent.java @@ -5,5 +5,5 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileTransferH public class FileDownloadedDeviceEvent extends GBDeviceEvent { public FileTransferHandler.DirectoryEntry directoryEntry; - + public String localPath; } 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 new file mode 100644 index 000000000..477d1f691 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitAsyncProcessor.java @@ -0,0 +1,63 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +import android.content.Context; +import android.os.Handler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class FitAsyncProcessor { + private static final Logger LOG = LoggerFactory.getLogger(FitAsyncProcessor.class); + + private final Context context; + private final GBDevice gbDevice; + private final Handler handler; + + public FitAsyncProcessor(final Context context, final GBDevice gbDevice) { + this.context = context; + this.gbDevice = gbDevice; + this.handler = new Handler(context.getMainLooper()); + } + + /** + * Process a list of files asynchronously. Callback is executed on the UI thread. + */ + public void process(final List files, final Callback callback) { + LOG.debug("Starting processor for {} files", files.size()); + + new Thread(() -> { + try { + int i = 0; + for (final File file : files) { + i++; + LOG.debug("Parsing {}", file); + + final int finalI = i; + FitAsyncProcessor.this.handler.post(() -> callback.onProgress(finalI)); + + try { + final FitImporter fitImporter = new FitImporter(context, gbDevice); + fitImporter.importFile(file); + } catch (final Exception ex) { + LOG.error("Exception while importing {}", file, ex); + } + } + } catch (final Exception e) { + LOG.error("Failed to parse from storage", e); + } + + FitAsyncProcessor.this.handler.post(callback::onFinish); + }).start(); + } + + public interface Callback { + void onProgress(final int perc); + + void onFinish(); + } +}