1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-24 22:10:55 +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() {
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);
}

View File

@ -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<FileType> supportedFileTypeList = new ArrayList<>();
private final List<File> 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 <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);
}
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();
}
});
}
}

View File

@ -5,5 +5,5 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileTransferH
public class FileDownloadedDeviceEvent extends GBDeviceEvent {
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();
}
}