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" /> + + + +