1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-07-09 15:11:34 +02:00

Garmin: Process downloaded fit files asynchronously

Fixes occasional ANR while syncing activity data.
This commit is contained in:
José Rebelo 2024-05-04 18:10:40 +01:00 committed by Daniele Gobbetti
parent a25d8eae30
commit f7bfd56d46
4 changed files with 134 additions and 47 deletions

View File

@ -134,18 +134,20 @@ public class FileTransferHandler implements MessageHandler {
private void saveFileToExternalStorage() { private void saveFileToExternalStorage() {
File dir; File dir;
File outputFile;
try { try {
dir = deviceSupport.getWritableExportDirectory(); dir = deviceSupport.getWritableExportDirectory();
File outputFile = new File(dir, currentlyDownloading.getFileName()); outputFile = new File(dir, currentlyDownloading.getFileName());
FileUtils.copyStreamToFile(new ByteArrayInputStream(currentlyDownloading.dataHolder.array()), outputFile); FileUtils.copyStreamToFile(new ByteArrayInputStream(currentlyDownloading.dataHolder.array()), outputFile);
outputFile.setLastModified(currentlyDownloading.directoryEntry.fileDate.getTime()); outputFile.setLastModified(currentlyDownloading.directoryEntry.fileDate.getTime());
} catch (final IOException e) {
} catch (IOException e) {
LOG.error("Failed to save file", e); LOG.error("Failed to save file", e);
return; // do not signal file as saved
} }
FileDownloadedDeviceEvent fileDownloadedDeviceEvent = new FileDownloadedDeviceEvent(); FileDownloadedDeviceEvent fileDownloadedDeviceEvent = new FileDownloadedDeviceEvent();
fileDownloadedDeviceEvent.directoryEntry = currentlyDownloading.directoryEntry; fileDownloadedDeviceEvent.directoryEntry = currentlyDownloading.directoryEntry;
fileDownloadedDeviceEvent.localPath = outputFile.getAbsolutePath();
deviceSupport.evaluateGBDeviceEvent(fileDownloadedDeviceEvent); deviceSupport.evaluateGBDeviceEvent(fileDownloadedDeviceEvent);
} }

View File

@ -14,13 +14,13 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue; import java.util.Queue;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; 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.NotificationSubscriptionDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent; 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.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.PredefinedLocalMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
@ -98,7 +98,9 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
private ICommunicator communicator; private ICommunicator communicator;
private MusicStateSpec musicStateSpec; private MusicStateSpec musicStateSpec;
private Timer musicStateTimer; private Timer musicStateTimer;
private final List<FileType> supportedFileTypeList = new ArrayList<>(); private final List<FileType> supportedFileTypeList = new ArrayList<>();
private final List<File> filesToProcess = new ArrayList<>();
public GarminSupport() { public GarminSupport() {
super(LOG); super(LOG);
@ -277,14 +279,7 @@ 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()) {
try { filesToProcess.add(new File(((FileDownloadedDeviceEvent) deviceEvent).localPath));
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);
}
} }
if (!getKeepActivityDataOnDevice()) { // delete file from watch upon successful download 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() { private void processDownloadQueue() {
moveFilesFromLegacyCache(); //TODO: remove before merging moveFilesFromLegacyCache(); //TODO: remove before merging
if (!filesToDownload.isEmpty() && !fileTransferHandler.isDownloading()) { if (!filesToDownload.isEmpty() && !fileTransferHandler.isDownloading()) {
if (!gbDevice.isBusy()) { if (!gbDevice.isBusy()) {
isBusyFetching = true;
GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data), "", true, 0, getContext()); 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().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data));
getDevice().sendDeviceUpdateIntent(getContext()); getDevice().sendDeviceUpdateIntent(getContext());
} }
try { while (!filesToDownload.isEmpty()) {
FileTransferHandler.DirectoryEntry directoryEntry = filesToDownload.remove(); final FileTransferHandler.DirectoryEntry directoryEntry = filesToDownload.remove();
while (checkFileExists(directoryEntry.getFileName()) || checkFileExists(directoryEntry.getLegacyFileName())) { if (checkFileExists(directoryEntry.getFileName()) || checkFileExists(directoryEntry.getLegacyFileName())) {
LOG.debug("File: {} already downloaded, not downloading again.", directoryEntry.getFileName()); LOG.debug("File: {} already downloaded, not downloading again.", directoryEntry.getFileName());
if (!getKeepActivityDataOnDevice()) { // delete file from watch if already downloaded if (!getKeepActivityDataOnDevice()) { // delete file from watch if already downloaded
sendOutgoingMessage(new SetFileFlagsMessage(directoryEntry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE)); 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) { if (downloadRequestMessage != null) {
sendOutgoingMessage(downloadRequestMessage); sendOutgoingMessage(downloadRequestMessage);
return;
} else { } else {
LOG.debug("File: {} already downloaded, not downloading again, from inside.", directoryEntry.getFileName()); 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(); getDevice().unsetBusyTask();
GB.updateTransferNotification(null, "", false, 100, getContext()); GB.updateTransferNotification(null, "", false, 100, getContext());
getDevice().sendDeviceUpdateIntent(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))) { // Keep the device marked as busy while we process the files asynchronously
getDevice().unsetBusyTask();
GB.updateTransferNotification(null, "", false, 100, getContext()); final FitAsyncProcessor fitAsyncProcessor = new FitAsyncProcessor(getContext(), getDevice());
getDevice().sendDeviceUpdateIntent(getContext()); final List <File> 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); GB.toast(getContext(), "Error deleting activity data", Toast.LENGTH_LONG, GB.ERROR, e);
} }
try { final FitAsyncProcessor fitAsyncProcessor = new FitAsyncProcessor(getContext(), getDevice());
int i = 0; fitAsyncProcessor.process(Arrays.asList(fitFiles), new FitAsyncProcessor.Callback() {
for (final File file : fitFiles) { @Override
i++; public void onProgress(final int i) {
LOG.debug("Parsing {}", file); GB.updateTransferNotification(
"Parsing fit files", "File " + i + " of " + fitFiles.length,
GB.updateTransferNotification("Parsing fit files", "File " + i + " of " + fitFiles.length, true, (i * 100) / fitFiles.length, getContext()); 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);
}
} }
} 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();
}
});
} }
} }

View File

@ -5,5 +5,5 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileTransferH
public class FileDownloadedDeviceEvent extends GBDeviceEvent { public class FileDownloadedDeviceEvent extends GBDeviceEvent {
public FileTransferHandler.DirectoryEntry directoryEntry; public FileTransferHandler.DirectoryEntry directoryEntry;
public String localPath;
} }

View File

@ -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<File> 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();
}
}