diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 5d628d3058..e17765f104 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -78,7 +78,8 @@
# Keep generated protobuf classes
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
-# Keep debug preference fragments that might only be referenced from xml
+# Keep preference fragments that might only be referenced from xml
+-keep class nodomain.freeyourgadget.gadgetbridge.activities.automations.** extends androidx.fragment.app.Fragment { *; }
-keep class nodomain.freeyourgadget.gadgetbridge.activities.debug.** extends androidx.fragment.app.Fragment { *; }
# jsoup 1.22.1 introduces support for re2j, but falls back to java Regex if not available
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fce10f1a1a..381d27479d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -226,6 +226,10 @@
android:name=".activities.automations.AutoExportZipSettingsActivity"
android:label="@string/pref_header_auto_export_zip"
android:parentActivityName=".activities.SettingsActivity" />
+
. */
+package nodomain.freeyourgadget.gadgetbridge.activities.automations
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.edit
+import androidx.preference.MultiSelectListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import nodomain.freeyourgadget.gadgetbridge.GBApplication
+import nodomain.freeyourgadget.gadgetbridge.R
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2
+import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs
+import org.slf4j.LoggerFactory
+
+class AutoExportGpxSettingsActivity : AbstractSettingsActivityV2() {
+ override fun newFragment(): PreferenceFragmentCompat {
+ return AutoExportGpxSettingsFragment()
+ }
+
+ companion object {
+ class AutoExportGpxSettingsFragment : AbstractPreferenceFragment() {
+ companion object {
+ private val LOG = LoggerFactory.getLogger(AutoExportGpxSettingsFragment::class.java)
+ }
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.auto_export_gpx_settings, rootKey)
+
+ setupCustomDirectoryPreference()
+ setupDeviceSelection()
+ }
+
+ private fun setupCustomDirectoryPreference() {
+ val gbPrefs = GBApplication.getPrefs()
+
+ val customDirectoryPicker = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result: ActivityResult? ->
+ if (result?.resultCode != RESULT_OK) {
+ return@registerForActivityResult
+ }
+ val uri = result.data?.data
+ LOG.info("Got {} for gpx export directory", uri)
+ if (uri == null) {
+ return@registerForActivityResult
+ }
+
+ requireContext().contentResolver.takePersistableUriPermission(
+ uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ )
+
+ gbPrefs.preferences.edit {
+ putString(GBPrefs.AUTO_EXPORT_GPX_DIRECTORY, uri.toString())
+ }
+
+ updateCustomDirectorySummary(uri.toString())
+ }
+
+ val customDirPref = findPreference(GBPrefs.AUTO_EXPORT_GPX_DIRECTORY)
+ customDirPref?.setOnPreferenceClickListener {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+ intent.addFlags(
+ Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ )
+ customDirectoryPicker.launch(intent)
+ true
+ }
+
+ val currentUri = gbPrefs.getString(GBPrefs.AUTO_EXPORT_GPX_DIRECTORY, "")
+ updateCustomDirectorySummary(currentUri)
+ }
+
+ private fun updateCustomDirectorySummary(uriString: String) {
+ val customDirPref = findPreference(GBPrefs.AUTO_EXPORT_GPX_DIRECTORY)
+ if (uriString.isEmpty()) {
+ customDirPref?.summary = getString(R.string.not_set)
+ } else {
+ customDirPref?.summary = uriString
+ }
+ }
+
+ private fun setupDeviceSelection() {
+ val selectedDevicesPref =
+ findPreference(GBPrefs.AUTO_EXPORT_GPX_SELECTED_DEVICES)
+
+ // Populate device list
+ val devices = GBApplication.app().deviceManager.devices
+ .filter { it.deviceCoordinator.supportsRecordedActivities(it) }
+ val deviceAddresses = devices.map { it.address }
+ val deviceNames = devices.map { it.aliasOrName }
+
+ selectedDevicesPref?.entryValues = deviceAddresses.toTypedArray()
+ selectedDevicesPref?.entries = deviceNames.toTypedArray()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/AutoGpxExporter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/AutoGpxExporter.java
new file mode 100644
index 0000000000..1df6529230
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/AutoGpxExporter.java
@@ -0,0 +1,112 @@
+package nodomain.freeyourgadget.gadgetbridge.export;
+
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.Nullable;
+import androidx.documentfile.provider.DocumentFile;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedOutputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
+import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
+
+public class AutoGpxExporter {
+ private static final Logger LOG = LoggerFactory.getLogger(AutoGpxExporter.class);
+
+ public static void doExport(final Context context,
+ final GBDevice gbDevice,
+ @Nullable final BaseActivitySummary summary,
+ final ActivityTrack activityTrack) {
+ final GBPrefs prefs = GBApplication.getPrefs();
+ final boolean enabled = prefs.getBoolean(GBPrefs.AUTO_EXPORT_GPX_ENABLED, false);
+ if (!enabled) {
+ LOG.debug("Auto gpx export is disabled");
+ return;
+ }
+
+ final Set selectedDevices = prefs.getStringSet(GBPrefs.AUTO_EXPORT_GPX_SELECTED_DEVICES, Collections.emptySet());
+ final boolean allDevices = prefs.getBoolean(GBPrefs.AUTO_EXPORT_GPX_ALL_DEVICES, true);
+ if (!allDevices && !selectedDevices.contains(gbDevice.getAddress())) {
+ LOG.debug("Skipping auto gpx export - not enabled for {}", gbDevice);
+ return;
+ }
+
+ final List points = activityTrack.getAllPoints();
+ final Optional firstValidPoint = points.stream().filter(p -> p.getLocation() != null).findFirst();
+ if (firstValidPoint.isEmpty()) {
+ LOG.warn("Not auto-exporting gpx, no valid points");
+ return;
+ }
+
+ final String directory = prefs.getString(GBPrefs.AUTO_EXPORT_GPX_DIRECTORY, "");
+ if (directory.isBlank()) {
+ LOG.warn("Not auto-exporting gpx, no directory specified");
+ return;
+ }
+
+ final String trackType;
+ if (summary != null) {
+ trackType = context.getString(ActivityKind.fromCode(summary.getActivityKind()).getLabel()).toLowerCase(Locale.ROOT);
+ } else {
+ trackType = "track";
+ }
+
+ final String isoDate = DateTimeUtils.formatIso8601(points.get(0).getTime());
+
+ final String fileName = FileUtils.makeValidFileName(isoDate + "-" + trackType + ".gpx");
+
+ try {
+ final Uri directoryUri = Uri.parse(directory);
+ final DocumentFile documentDir = DocumentFile.fromTreeUri(context, directoryUri);
+
+ if (documentDir == null || !documentDir.exists() || !documentDir.canWrite()) {
+ LOG.error("Cannot write to directory: {}", directory);
+ // TODO notification?
+ return;
+ }
+
+ final DocumentFile existingFile = documentDir.findFile(fileName);
+ if (existingFile != null) {
+ LOG.debug("File already exists, will not overwrite: {}", fileName);
+ return;
+ }
+
+ final DocumentFile targetFile = documentDir.createFile("application/gpx+xml", fileName);
+ if (targetFile == null) {
+ LOG.error("Failed to create file: {}", fileName);
+ // TODO notification?
+ return;
+ }
+
+ try (OutputStream outputStream = context.getContentResolver().openOutputStream(targetFile.getUri());
+ BufferedOutputStream bos = new BufferedOutputStream(outputStream)) {
+ final GPXExporter exporter = new GPXExporter();
+ exporter.performExport(activityTrack, bos);
+ }
+
+ LOG.info("Auto-exported GPX to: {}", targetFile.getUri());
+ } catch (final ActivityTrackExporter.GPXTrackEmptyException e) {
+ LOG.debug("Activity does not contain any points");
+ } catch (final Exception e) {
+ LOG.error("Failed to auto-export GPX", e);
+ // TODO notification
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java
index 3d27f77242..a990b7b1cf 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java
@@ -24,9 +24,11 @@ import androidx.annotation.Nullable;
import org.jetbrains.annotations.TestOnly;
import org.xmlpull.v1.XmlSerializer;
+import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.OutputStream;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
@@ -79,35 +81,41 @@ public class GPXExporter implements ActivityTrackExporter {
@Override
public void performExport(ActivityTrack track, File targetFile) throws IOException, GPXTrackEmptyException {
+ try (FileOutputStream outputStream = new FileOutputStream(targetFile);
+ BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
+ performExport(track, bufferedOutputStream);
+ }
+ }
+
+ public void performExport(ActivityTrack track, OutputStream outputStream) throws IOException, GPXTrackEmptyException {
String encoding = StandardCharsets.UTF_8.name();
XmlSerializer ser = Xml.newSerializer();
- try (FileOutputStream outputStream = new FileOutputStream(targetFile)) {
- ser.setOutput(outputStream, encoding);
- //ser.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
- ser.startDocument(encoding, Boolean.TRUE);
- ser.setPrefix("xsi", NS_XSI_URI);
- ser.setPrefix(NS_TRACKPOINT_EXTENSION, NS_TRACKPOINT_EXTENSION_URI);
- ser.setPrefix(NS_GPX_PREFIX, NS_GPX_URI);
- ser.setPrefix(OPENTRACKS_PREFIX, OPENTRACKS_NAMESPACE_URI);
- ser.startTag(NS_GPX_URI, "gpx");
- ser.attribute(null, "version", "1.1");
- if (creator != null) {
- ser.attribute(null, "creator", creator);
- } else {
- ser.attribute(null, "creator", GBApplication.app().getNameAndVersion());
- }
- ser.attribute(NS_XSI_URI, "schemaLocation",NS_GPX_URI + " " + TOPOGRAFIX_NAMESPACE_XSD
- + " " + NS_TRACKPOINT_EXTENSION_URI + " " + TRACKPOINT_EXTENSION_XSD
- + " " + OPENTRACKS_NAMESPACE_URI + " " + OPENTRACKS_XSD);
+ ser.setOutput(outputStream, encoding);
+ //ser.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ ser.startDocument(encoding, Boolean.TRUE);
+ ser.setPrefix("xsi", NS_XSI_URI);
+ ser.setPrefix(NS_TRACKPOINT_EXTENSION, NS_TRACKPOINT_EXTENSION_URI);
+ ser.setPrefix(NS_GPX_PREFIX, NS_GPX_URI);
+ ser.setPrefix(OPENTRACKS_PREFIX, OPENTRACKS_NAMESPACE_URI);
- exportMetadata(ser, track);
- exportTrack(ser, track);
-
- ser.endTag(NS_GPX_URI, "gpx");
- ser.endDocument();
- ser.flush();
+ ser.startTag(NS_GPX_URI, "gpx");
+ ser.attribute(null, "version", "1.1");
+ if (creator != null) {
+ ser.attribute(null, "creator", creator);
+ } else {
+ ser.attribute(null, "creator", GBApplication.app().getNameAndVersion());
}
+ ser.attribute(NS_XSI_URI, "schemaLocation",NS_GPX_URI + " " + TOPOGRAFIX_NAMESPACE_XSD
+ + " " + NS_TRACKPOINT_EXTENSION_URI + " " + TRACKPOINT_EXTENSION_XSD
+ + " " + OPENTRACKS_NAMESPACE_URI + " " + OPENTRACKS_XSD);
+
+ exportMetadata(ser, track);
+ exportTrack(ser, track);
+
+ ser.endTag(NS_GPX_URI, "gpx");
+ ser.endDocument();
+ ser.flush();
}
private void exportMetadata(XmlSerializer ser, ActivityTrack track) throws IOException {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSActivityTrack.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSActivityTrack.java
index da591589e3..ff1936ac20 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSActivityTrack.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/banglejs/BangleJSActivityTrack.java
@@ -34,8 +34,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
-import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
-import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
+import nodomain.freeyourgadget.gadgetbridge.export.AutoGpxExporter;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
@@ -204,40 +203,8 @@ class BangleJSActivityTrack {
track.addTrackPoint(banglePoint.toActivityPoint());
}
- ActivityTrackExporter exporter = new GPXExporter();
- String trackType = "track";
- switch (ActivityKind.fromCode(summary.getActivityKind())) {
- case CYCLING:
- trackType = context.getString(R.string.activity_type_biking);
- break;
- case RUNNING:
- trackType = context.getString(R.string.activity_type_running);
- break;
- case WALKING:
- trackType = context.getString(R.string.activity_type_walking);
- break;
- case HIKING:
- trackType = context.getString(R.string.activity_type_hiking);
- break;
- case CLIMBING:
- trackType = context.getString(R.string.activity_type_climbing);
- break;
- case SWIMMING:
- trackType = context.getString(R.string.activity_type_swimming);
- break;
- }
-
- String fileName = FileUtils.makeValidFileName("gadgetbridge-" + trackType.toLowerCase() + "-" + summary.getName() + ".gpx");
- dir = new File(FileUtils.getExternalFilesDir() + "/" + FileUtils.makeValidFileName(device.getName()));
- File targetFile = new File(dir, fileName);
-
if (hasGPXReading /*|| hasHRMReading*/) {
- try {
- exporter.performExport(track, targetFile);
- summary.setGpxTrack(targetFile.getAbsolutePath());
- } catch (ActivityTrackExporter.GPXTrackEmptyException ex) {
- GB.toast(context, "This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.ERROR, ex);
- }
+ AutoGpxExporter.doExport(context, device, summary, track);
}
//summary.setSummaryData(null); // remove json before saving to database,
@@ -253,10 +220,10 @@ class BangleJSActivityTrack {
GB.toast(context, "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex);
}
- LOG.debug("Activity track:\n" + track.getSegments());
+ LOG.debug("Activity track:\n{}", track.getSegments());
- } catch (IOException e) {
- LOG.error("IOException when parsing fetched CSV: " + e);
+ } catch (Exception e) {
+ LOG.error("IOException when parsing fetched CSV", e);
}
stopAndRestartTimeout(device,context);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfActivitySync.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfActivitySync.java
index 38b69d0be4..dc0eb996bd 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfActivitySync.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfActivitySync.java
@@ -19,19 +19,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.cmfwatchpro;
import android.content.Context;
import android.widget.Toast;
-import androidx.annotation.Nullable;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.File;
-import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
-import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
@@ -46,7 +41,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfWorko
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout.CmfActivityTrackProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout.CmfWorkoutSummaryParser;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
-import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.CmfActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.CmfHeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.CmfSleepSessionSample;
@@ -57,13 +51,10 @@ import nodomain.freeyourgadget.gadgetbridge.entities.CmfWorkoutGpsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
-import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
-import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
+import nodomain.freeyourgadget.gadgetbridge.export.AutoGpxExporter;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
-import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
-import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class CmfActivitySync {
@@ -505,64 +496,7 @@ public class CmfActivitySync {
}
// Save the gpx file
- final File gpxFile = exportGpx(summary, activityTrack);
- if (gpxFile == null) {
- return;
- }
-
- // Update the summary in the db with the gpx path
- try (DBHandler dbHandler = GBApplication.acquireDB()) {
- final DaoSession session = dbHandler.getDaoSession();
- final Device device = DBHelper.getDevice(mSupport.getDevice(), session);
- final User user = DBHelper.getUser(session);
-
- final BaseActivitySummaryDao summaryDao = session.getBaseActivitySummaryDao();
- final QueryBuilder qb = summaryDao.queryBuilder();
- qb.where(BaseActivitySummaryDao.Properties.StartTime.eq(summary.getStartTime()));
- qb.where(BaseActivitySummaryDao.Properties.DeviceId.eq(device.getId()));
- qb.where(BaseActivitySummaryDao.Properties.UserId.eq(user.getId()));
- final List summaries = qb.build().list();
-
- if (summaries.isEmpty()) {
- LOG.warn("Failed to find existing summary in db - this should never happen");
- return;
- }
- if (summaries.size() > 1) {
- LOG.warn("Found multiple summaries in db - this should never happen");
- }
-
- final BaseActivitySummary summaryToUpdate = summaries.get(0);
- summaryToUpdate.setGpxTrack(gpxFile.getAbsolutePath());
- session.getBaseActivitySummaryDao().insertOrReplace(summaryToUpdate);
- } catch (final Exception e) {
- LOG.error("Failed to update summary with gpx path", e);
- }
- }
-
- @Nullable
- private File exportGpx(final BaseActivitySummary summary, final ActivityTrack activityTrack) {
- final GPXExporter exporter = new GPXExporter();
-
- final String gpxFileName = FileUtils.makeValidFileName("gadgetbridge-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx");
- final File gpxTargetFile;
- try {
- gpxTargetFile = new File(FileUtils.getExternalFilesDir(), gpxFileName);
- } catch (final IOException e) {
- LOG.error("Failed to get external files dir", e);
- return null;
- }
-
- try {
- exporter.performExport(activityTrack, gpxTargetFile);
- } catch (final ActivityTrackExporter.GPXTrackEmptyException e) {
- LOG.warn("Gpx is empty");
- return null;
- } catch (IOException e) {
- LOG.error("Failed to write gpx", e);
- return null;
- }
-
- return gpxTargetFile;
+ AutoGpxExporter.doExport(getContext(), getDevice(), summary, activityTrack);
}
private Context getContext() {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/fetch/FetchSportsDetailsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/fetch/FetchSportsDetailsOperation.java
index acad85b472..9dddfc4aec 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/fetch/FetchSportsDetailsOperation.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/fetch/FetchSportsDetailsOperation.java
@@ -17,7 +17,6 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
-import android.text.format.DateUtils;
import android.widget.Toast;
import org.slf4j.Logger;
@@ -37,9 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
-import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
-import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
-import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.export.AutoGpxExporter;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
@@ -113,48 +110,7 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
try {
final ActivityTrack track = detailsParser.parse(buffer.toByteArray());
- final ActivityTrackExporter exporter = new GPXExporter();
- final String trackType;
- switch (ActivityKind.fromCode(summary.getActivityKind())) {
- case CYCLING:
- trackType = getContext().getString(R.string.activity_type_biking);
- break;
- case RUNNING:
- trackType = getContext().getString(R.string.activity_type_running);
- break;
- case WALKING:
- trackType = getContext().getString(R.string.activity_type_walking);
- break;
- case HIKING:
- trackType = getContext().getString(R.string.activity_type_hiking);
- break;
- case CLIMBING:
- trackType = getContext().getString(R.string.activity_type_climbing);
- break;
- case SWIMMING:
- trackType = getContext().getString(R.string.activity_type_swimming);
- break;
- default:
- trackType = "track";
- break;
- }
-
- final String fileName = FileUtils.makeValidFileName("gadgetbridge-" + trackType.toLowerCase() + "-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx");
- final File targetFile = new File(FileUtils.getExternalFilesDir(), fileName);
-
- boolean exportGpxSuccess = true;
- try {
- exporter.performExport(track, targetFile);
- } catch (final ActivityTrackExporter.GPXTrackEmptyException ex) {
- exportGpxSuccess = false;
- }
-
- try (DBHandler dbHandler = GBApplication.acquireDB()) {
- if (exportGpxSuccess) {
- summary.setGpxTrack(targetFile.getAbsolutePath());
- }
- dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary);
- }
+ AutoGpxExporter.doExport(getContext(), getDevice(), summary, track);
} catch (final Exception e) {
GB.toast(getContext(), "Error saving activity details: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
// #4549 - we do not return false here, since this might cause the same activity to be fetched over and over again
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java
index 4c5b5b9ded..c9461714b1 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java
@@ -116,8 +116,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSampleDao;
-import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
-import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
+import nodomain.freeyourgadget.gadgetbridge.export.AutoGpxExporter;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -248,7 +247,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetN
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetWorkModeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
-import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.MediaManager;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
@@ -3378,42 +3376,7 @@ public class HuaweiSupportProvider {
track.addTrackPoint(activityPoint);
}
- String filename = FileUtils.makeValidFileName("workout_" + fileRequest.getWorkoutId() + "_" + points[0].timestamp + ".gpx");
- File targetFile;
- try {
- targetFile = new File(
- getDevice().getDeviceCoordinator().getWritableExportDirectory(getDevice(), true),
- filename
- );
- } catch (IOException e) {
- GB.toast(context, "Could not open Workout GPS file to write to", Toast.LENGTH_SHORT, GB.ERROR, e);
- LOG.error("Could not open Workout GPS file to write to", e);
- syncState.stopWorkoutGpsDownload();
- return;
- }
-
- GPXExporter exporter = new GPXExporter();
- exporter.setCreator(GBApplication.app().getNameAndVersion());
- try {
- exporter.performExport(track, targetFile);
- } catch (IOException | ActivityTrackExporter.GPXTrackEmptyException e) {
- GB.toast(context, "Failed to export Workout GPX file", Toast.LENGTH_SHORT, GB.ERROR, e);
- LOG.error("Failed to export Workout GPX file", e);
- syncState.stopWorkoutGpsDownload();
- return;
- }
-
- try (DBHandler db = GBApplication.acquireDB()) {
- DaoSession daoSession = db.getDaoSession();
- HuaweiWorkoutSummarySample sample = daoSession.getHuaweiWorkoutSummarySampleDao().load(databaseId);
- sample.setGpxFileLocation(targetFile.getAbsolutePath());
- sample.update();
- } catch (Exception e) {
- GB.toast(context, "Failed to save Workout GPX file location", Toast.LENGTH_SHORT, GB.ERROR, e);
- LOG.error("Failed to save Workout GPX file location", e);
- syncState.stopWorkoutGpsDownload();
- return;
- }
+ AutoGpxExporter.doExport(getContext(), getDevice(), null, track);
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(databaseId);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutGpsParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutGpsParser.java
index 110c835c01..cd36f92491 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutGpsParser.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutGpsParser.java
@@ -36,8 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
-import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
-import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
+import nodomain.freeyourgadget.gadgetbridge.export.AutoGpxExporter;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
@@ -46,8 +45,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileFetcher;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityParser;
-import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
-import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class WorkoutGpsParser extends XiaomiActivityParser {
@@ -129,13 +126,14 @@ public class WorkoutGpsParser extends XiaomiActivityParser {
return false;
}
+ final BaseActivitySummary summary;
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
// Find the matching summary
- final BaseActivitySummary summary = findOrCreateBaseActivitySummary(session, device, user, fileId);
+ summary = findOrCreateBaseActivitySummary(session, device, user, fileId);
// Set the info on the activity track
activityTrack.setUser(user);
@@ -146,22 +144,6 @@ public class WorkoutGpsParser extends XiaomiActivityParser {
final File rawBytesFile = XiaomiActivityFileFetcher.getRawFile(gbDevice, fileId);
// Save the gpx file
- final GPXExporter exporter = new GPXExporter();
-
- final String gpxFileName = FileUtils.makeValidFileName("gadgetbridge-" + DateTimeUtils.formatIso8601(fileId.getTimestamp()) + ".gpx");
- final File gpxTargetFile = new File(FileUtils.getExternalFilesDir(), gpxFileName);
-
- boolean exportGpxSuccess = true;
- try {
- exporter.performExport(activityTrack, gpxTargetFile);
- } catch (final ActivityTrackExporter.GPXTrackEmptyException ex) {
- exportGpxSuccess = false;
- GB.toast(context, "This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.ERROR, ex);
- }
-
- if (exportGpxSuccess) {
- summary.setGpxTrack(gpxTargetFile.getAbsolutePath());
- }
if (rawBytesFile != null) {
summary.setRawDetailsPath(rawBytesFile.getAbsolutePath());
}
@@ -171,6 +153,8 @@ public class WorkoutGpsParser extends XiaomiActivityParser {
return false;
}
+ AutoGpxExporter.doExport(context, gbDevice, summary, activityTrack);
+
return true;
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java
index 7cf6192e63..390ccfa809 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java
@@ -81,6 +81,12 @@ public class GBPrefs extends Prefs {
public static final String AUTO_EXPORT_ZIP_LAST_EXECUTION = "zip_auto_export_last_execution";
public static final String AUTO_EXPORT_ZIP_NEXT_EXECUTION = "zip_auto_export_next_execution";
+ // GPX export
+ public static final String AUTO_EXPORT_GPX_ENABLED = "gpx_auto_export_enabled";
+ public static final String AUTO_EXPORT_GPX_DIRECTORY = "gpx_auto_export_directory";
+ public static final String AUTO_EXPORT_GPX_ALL_DEVICES = "gpx_auto_export_all_devices";
+ public static final String AUTO_EXPORT_GPX_SELECTED_DEVICES = "gpx_auto_export_selected_devices";
+
// Intent API
public static final String INTENT_API_BROADCAST_EXPORT_DB = "intent_api_broadcast_export";
public static final String INTENT_API_BROADCAST_EXPORT_ZIP = "intent_api_broadcast_zip_export";
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1bb6d0eea7..0dd24c846d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -647,10 +647,17 @@
Auto export
Auto export database
Auto export zip
+ Auto export GPX tracks
Auto export enabled
Export location
Export interval
Export every %d hour
+
+ Automatically export GPX files when syncing activity tracks
+ Export directory
+ Export GPX tracks from all devices
+ Selected devices
+ Choose which devices to export GPX tracks from
Auto fetch
Auto fetch activity data
diff --git a/app/src/main/res/xml/auto_export_gpx_settings.xml b/app/src/main/res/xml/auto_export_gpx_settings.xml
new file mode 100644
index 0000000000..0780bf79ed
--- /dev/null
+++ b/app/src/main/res/xml/auto_export_gpx_settings.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/automations_settings.xml b/app/src/main/res/xml/automations_settings.xml
index 6b526ee773..128f4a66ea 100644
--- a/app/src/main/res/xml/automations_settings.xml
+++ b/app/src/main/res/xml/automations_settings.xml
@@ -20,6 +20,14 @@
android:targetPackage="@string/applicationId" />
+
+
+
+