Garmin: Allow manual import of activity files

This commit is contained in:
José Rebelo 2025-02-23 19:48:29 +00:00
parent bbcd09dae1
commit b81784f3b9
7 changed files with 112 additions and 2 deletions

View File

@ -93,6 +93,9 @@ public class ActivitySummariesGpsFragment extends AbstractGBFragment {
public static List<ActivityPoint> getActivityPoints(final File trackFile) {
final List<ActivityPoint> points = new ArrayList<>();
if (trackFile == null) {
return points;
}
if (trackFile.getName().endsWith(".gpx")) {
try (FileInputStream inputStream = new FileInputStream(trackFile)) {
final GpxParser gpxParser = new GpxParser(inputStream);

View File

@ -223,6 +223,7 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
connection.add(R.xml.devicesettings_high_mtu);
final List<Integer> developer = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER);
developer.add(R.xml.devicesettings_import_activity_files);
developer.add(R.xml.devicesettings_keep_activity_data_on_device);
developer.add(R.xml.devicesettings_fetch_unknown_files);

View File

@ -22,7 +22,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@ -35,9 +38,12 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps.GarminAgpsStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitAsyncProcessor;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -62,6 +68,64 @@ public class GarminSettingsCustomizer implements DeviceSpecificSettingsCustomize
});
}
final Preference prefImportActivityFiles = handler.findPreference("import_activity_files");
if (prefImportActivityFiles != null) {
final ActivityResultLauncher<String[]> activityFileChooser = handler.registerForActivityResult(
new ActivityResultContracts.OpenMultipleDocuments(),
localUris -> {
LOG.info("Files to import: {}", localUris);
if (localUris != null) {
final List<File> filesToProcess = new ArrayList<>(localUris.size());
final Context context = handler.getContext();
for (final Uri uri : localUris) {
final File file;
try {
file = File.createTempFile("activity-files-import", ".bin", context.getCacheDir());
file.deleteOnExit();
FileUtils.copyURItoFile(context, uri, file);
filesToProcess.add(file);
} catch (final IOException e) {
LOG.error("Failed to create temp file for activity file", e);
}
}
if (filesToProcess.isEmpty()) {
return;
}
final FitAsyncProcessor fitAsyncProcessor = new FitAsyncProcessor(context, handler.getDevice());
final long[] lastNotificationUpdateTs = new long[]{System.currentTimeMillis()};
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 " + filesToProcess.size(),
true,
(i * 100) / filesToProcess.size(), context
);
}
}
@Override
public void onFinish() {
GB.updateTransferNotification("", "", false, 100, context);
GB.toast("Parsed " + filesToProcess.size() + " files", Toast.LENGTH_SHORT, GB.INFO);
handler.getDevice().sendDeviceUpdateIntent(context);
}
});
}
}
);
prefImportActivityFiles.setOnPreferenceClickListener(preference -> {
activityFileChooser.launch(new String[]{"*/*"});
return true;
});
}
final PreferenceCategory prefAgpsHeader = handler.findPreference(DeviceSettingsPreferenceConst.PREF_HEADER_AGPS);
if (prefAgpsHeader != null) {
final List<String> urls = prefs.getList(GarminPreferences.PREF_AGPS_KNOWN_URLS, Collections.emptyList(), "\n");

View File

@ -8,9 +8,12 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
@ -78,6 +81,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitStressLevel;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitTimeInZone;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FitImporter {
@ -329,12 +333,41 @@ public class FitImporter {
return;
}
// If the file is not yet on the export directory (eg. we're importing from phone storage), copy it
File finalExportFile = file;
try {
final File exportDirectory = gbDevice.getDeviceCoordinator().getWritableExportDirectory(gbDevice);
if (!file.getAbsolutePath().startsWith(exportDirectory.getAbsolutePath())) {
final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ROOT);
final StringBuilder sb = new StringBuilder(fileId.getType().name());
if (fileId.getTimeCreated() != null && fileId.getTimeCreated() != 0) {
sb.append("_").append(SDF.format(new Date(fileId.getTimeCreated() * 1000L)));
}
sb.append(".fit");
final File exportFile = new File(exportDirectory, sb.toString());
if (exportFile.isFile()) {
// Prevent overwrite
LOG.warn("Fit file {} already exists as {}", file, exportFile);
} else {
LOG.debug("Copying {} to {}", file, exportFile);
FileUtils.copyFile(file, exportFile);
exportFile.setLastModified(file.lastModified());
}
finalExportFile = exportFile;
}
} catch (final Exception e) {
LOG.error("Failed to copy file to export directory", e);
}
try (DBHandler handler = GBApplication.acquireDB()) {
final DaoSession session = handler.getDaoSession();
switch (fileId.getType()) {
case ACTIVITY:
persistWorkout(file, session);
persistWorkout(finalExportFile, session);
break;
case MONITOR:
persistActivitySamples(session);

View File

@ -144,7 +144,6 @@ public class FileUtils {
}
try (InputStream fin = new BufferedInputStream(in)) {
copyStreamToFile(fin, destFile);
fin.close();
}
}

View File

@ -3505,6 +3505,8 @@
<string name="battery_full_threshold">Full battery threshold</string>
<string name="default_percentage">Default (%1$d%%)</string>
<string name="battery_percentage_str">%1$s%%</string>
<string name="pref_import_activity_files_title">Import activity files</string>
<string name="pref_import_activity_files_summary">Manually import activity files from the phone storage</string>
<string name="pref_fetch_unknown_files_title">Fetch unknown files</string>
<string name="pref_fetch_unknown_files_summary">Fetch unknown activity files from the watch. They will not be processed, but will be saved in the phone.</string>
<string name="cannot_upload_watchface_too_many_watchfaces_installed">"Cannot upload watchface, too many watchfaces installed"</string>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:icon="@drawable/ic_file_upload"
android:key="import_activity_files"
android:summary="@string/pref_import_activity_files_summary"
android:title="@string/pref_import_activity_files_title" />
</androidx.preference.PreferenceScreen>