mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Save track to GPX file when stopping OpenTracks recording
This commit is contained in:
committed by
Arjan Schrijver
parent
26fdfb5df0
commit
3840595b35
+36
-2
@@ -23,18 +23,31 @@ import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
|
||||
|
||||
public class OpenTracksContentObserver extends ContentObserver {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OpenTracksContentObserver.class);
|
||||
|
||||
private Context mContext;
|
||||
private Uri tracksUri;
|
||||
private Uri trackpointsUri;
|
||||
private int protocolVersion;
|
||||
private int totalTimeMillis;
|
||||
private float totalDistanceMeter;
|
||||
private final List<ActivityPoint> activityPoints;
|
||||
|
||||
private long previousTimeMillis = 0;
|
||||
private float previousDistanceMeter = 0;
|
||||
private long lastTrackPointId;
|
||||
|
||||
public int getTotalTimeMillis() {
|
||||
return totalTimeMillis;
|
||||
@@ -42,6 +55,9 @@ public class OpenTracksContentObserver extends ContentObserver {
|
||||
public float getTotalDistanceMeter() {
|
||||
return totalDistanceMeter;
|
||||
}
|
||||
public List<ActivityPoint> getActivityPoints() {
|
||||
return activityPoints;
|
||||
}
|
||||
|
||||
public long getTimeMillisChange() {
|
||||
/**
|
||||
@@ -59,13 +75,16 @@ public class OpenTracksContentObserver extends ContentObserver {
|
||||
return distanceMeterDelta;
|
||||
}
|
||||
|
||||
|
||||
public OpenTracksContentObserver(Context context, final Uri tracksUri, final int protocolVersion) {
|
||||
public OpenTracksContentObserver(Context context, final Uri tracksUri, final Uri trackpointsUri, final int protocolVersion) {
|
||||
super(new Handler());
|
||||
this.mContext = context;
|
||||
this.tracksUri = tracksUri;
|
||||
this.trackpointsUri = trackpointsUri;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.previousTimeMillis = System.currentTimeMillis();
|
||||
this.activityPoints = new ArrayList<>();
|
||||
|
||||
LOG.debug("Initializing OpenTracksContentObserver...");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,6 +100,21 @@ public class OpenTracksContentObserver extends ContentObserver {
|
||||
totalDistanceMeter = statistics.getTotalDistanceMeter();
|
||||
}
|
||||
}
|
||||
if (trackpointsUri.toString().startsWith(uri.toString())) {
|
||||
final TrackPointsBySegments trackPointsBySegments = TrackPoint.readTrackPointsBySegments(mContext.getContentResolver(), trackpointsUri, lastTrackPointId, protocolVersion);
|
||||
if (!trackPointsBySegments.isEmpty()) {
|
||||
for (List<TrackPoint> segment : trackPointsBySegments.segments()) {
|
||||
for (TrackPoint trackPoint : segment) {
|
||||
lastTrackPointId = trackPoint.getTrackPointId();
|
||||
ActivityPoint activityPoint = new ActivityPoint();
|
||||
activityPoint.setLocation(new GPSCoordinate(trackPoint.getLongitude(), trackPoint.getLatitude()));
|
||||
activityPoint.setTime(new Date());
|
||||
activityPoints.add(activityPoint);
|
||||
LOG.debug("Trackpoint received from OpenTracks: {}/{}", trackPoint.getLatitude(), trackPoint.getLongitude());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
|
||||
+35
-1
@@ -27,10 +27,21 @@ import android.widget.Toast;
|
||||
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.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
@@ -76,10 +87,13 @@ public class OpenTracksController extends Activity {
|
||||
gbApp.getOpenTracksObserver().unregister();
|
||||
}
|
||||
Uri tracksUri = uris.get(0);
|
||||
Uri trackpointsUri = uris.get(1);
|
||||
LOG.info("Registering OpenTracksContentObserver with tracks URI: " + tracksUri);
|
||||
gbApp.setOpenTracksObserver(new OpenTracksContentObserver(this, tracksUri, protocolVersion));
|
||||
LOG.info("Registering OpenTracksContentObserver with trackpoints URI: " + trackpointsUri);
|
||||
gbApp.setOpenTracksObserver(new OpenTracksContentObserver(this, tracksUri, trackpointsUri, protocolVersion));
|
||||
try {
|
||||
getContentResolver().registerContentObserver(tracksUri, false, gbApp.getOpenTracksObserver());
|
||||
getContentResolver().registerContentObserver(trackpointsUri, false, gbApp.getOpenTracksObserver());
|
||||
} catch (final SecurityException se) {
|
||||
LOG.error("Error registering OpenTracksContentObserver", se);
|
||||
}
|
||||
@@ -138,6 +152,7 @@ public class OpenTracksController extends Activity {
|
||||
sendIntent(context, "de.dennisguse.opentracks.publicapi.StopRecording", null, null);
|
||||
OpenTracksContentObserver openTracksObserver = GBApplication.app().getOpenTracksObserver();
|
||||
if (openTracksObserver != null) {
|
||||
saveToGpx(openTracksObserver.getActivityPoints());
|
||||
openTracksObserver.finish();
|
||||
}
|
||||
GBApplication.app().setOpenTracksObserver(null);
|
||||
@@ -151,4 +166,23 @@ public class OpenTracksController extends Activity {
|
||||
stopRecording(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static void saveToGpx(List<ActivityPoint> activityPoints) {
|
||||
final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault());
|
||||
final String gpxName = sdf.format(new Date());
|
||||
|
||||
final ActivityTrack activityTrack = new ActivityTrack();
|
||||
activityTrack.setName(gpxName);
|
||||
activityTrack.addTrackPoints(activityPoints);
|
||||
|
||||
try {
|
||||
final File gpxDir = FileUtils.getExternalFilesDir();
|
||||
final File gpxFile = new File(gpxDir, gpxName + ".gpx");
|
||||
final GPXExporter gpxExporter = new GPXExporter();
|
||||
gpxExporter.performExport(activityTrack, gpxFile);
|
||||
LOG.info("Saved GPX received from OpenTracks to {}", gpxFile.getPath());
|
||||
} catch (IOException | ActivityTrackExporter.GPXTrackEmptyException e) {
|
||||
LOG.error("Error while writing generated GPX file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/* Copyright (C) 2025 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class was copied and modified from
|
||||
* https://github.com/OpenTracksApp/OSMDashboard/blob/v4.3.0/src/main/java/de/storchp/opentracks/osmplugin/dashboardapi/TrackPoint.java
|
||||
*/
|
||||
public class TrackPoint {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TrackPoint.class);
|
||||
|
||||
public static final String _ID = "_id";
|
||||
public static final String TRACKID = "trackid";
|
||||
public static final String LONGITUDE = "longitude";
|
||||
public static final String LATITUDE = "latitude";
|
||||
public static final String TIME = "time";
|
||||
public static final String TYPE = "type";
|
||||
public static final String SPEED = "speed";
|
||||
public static final double PAUSE_LATITUDE = 100.0;
|
||||
public static final double LAT_LON_FACTOR = 1E6;
|
||||
|
||||
public static final String[] PROJECTION_V1 = {
|
||||
_ID,
|
||||
TRACKID,
|
||||
LATITUDE,
|
||||
LONGITUDE,
|
||||
TIME,
|
||||
SPEED
|
||||
};
|
||||
|
||||
public static final String[] PROJECTION_V2 = {
|
||||
_ID,
|
||||
TRACKID,
|
||||
LATITUDE,
|
||||
LONGITUDE,
|
||||
TIME,
|
||||
TYPE,
|
||||
SPEED
|
||||
};
|
||||
|
||||
private final long trackPointId;
|
||||
private final long trackId;
|
||||
private final double latitude;
|
||||
private final double longitude;
|
||||
private final boolean pause;
|
||||
private final double speed;
|
||||
|
||||
public TrackPoint(long trackId, long trackPointId, double latitude, double longitude, Integer type, double speed) {
|
||||
this.trackId = trackId;
|
||||
this.trackPointId = trackPointId;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.pause = type != null ? type == 3 : latitude == PAUSE_LATITUDE;
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public boolean hasValidLocation() {
|
||||
return latitude != 0 || longitude != 0;
|
||||
}
|
||||
|
||||
public boolean isPause() {
|
||||
return pause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the TrackPoints from the Content Uri and split by segments.
|
||||
* Pause TrackPoints and different Track IDs split the segments.
|
||||
*/
|
||||
public static TrackPointsBySegments readTrackPointsBySegments(ContentResolver resolver, Uri data, long lastTrackPointId, int protocolVersion) {
|
||||
var segments = new ArrayList<List<TrackPoint>>();
|
||||
var projection = PROJECTION_V2;
|
||||
var typeQuery = " AND " + TrackPoint.TYPE + " IN (-2, -1, 0, 1, 3)";
|
||||
if (protocolVersion < 2) { // fallback to old Dashboard API
|
||||
projection = PROJECTION_V1;
|
||||
typeQuery = "";
|
||||
}
|
||||
try (Cursor cursor = resolver.query(data, projection, TrackPoint._ID + "> ?" + typeQuery, new String[]{Long.toString(lastTrackPointId)}, null)) {
|
||||
TrackPoint lastTrackPoint = null;
|
||||
List<TrackPoint> segment = null;
|
||||
while (cursor.moveToNext()) {
|
||||
var trackPointId = cursor.getLong(cursor.getColumnIndexOrThrow(TrackPoint._ID));
|
||||
var trackId = cursor.getLong(cursor.getColumnIndexOrThrow(TrackPoint.TRACKID));
|
||||
var latitude = cursor.getInt(cursor.getColumnIndexOrThrow(TrackPoint.LATITUDE)) / LAT_LON_FACTOR;
|
||||
var longitude = cursor.getInt(cursor.getColumnIndexOrThrow(TrackPoint.LONGITUDE)) / LAT_LON_FACTOR;
|
||||
var typeIndex = cursor.getColumnIndex(TrackPoint.TYPE);
|
||||
var speed = cursor.getDouble(cursor.getColumnIndexOrThrow(TrackPoint.SPEED));
|
||||
|
||||
Integer type = null;
|
||||
if (typeIndex > -1) {
|
||||
type = cursor.getInt(typeIndex);
|
||||
}
|
||||
|
||||
if (lastTrackPoint == null || lastTrackPoint.trackId != trackId) {
|
||||
segment = new ArrayList<>();
|
||||
segments.add(segment);
|
||||
}
|
||||
|
||||
lastTrackPoint = new TrackPoint(trackId, trackPointId, latitude, longitude, type, speed);
|
||||
if (lastTrackPoint.hasValidLocation()) {
|
||||
segment.add(lastTrackPoint);
|
||||
} else if (!lastTrackPoint.isPause()) {
|
||||
// invalid trackpoint
|
||||
}
|
||||
if (lastTrackPoint.isPause()) {
|
||||
if (!lastTrackPoint.hasValidLocation()) {
|
||||
if (!segment.isEmpty()) {
|
||||
var previousTrackpoint = segment.get(segment.size() - 1);
|
||||
if (previousTrackpoint.hasValidLocation()) {
|
||||
segment.add(new TrackPoint(trackId, trackPointId, previousTrackpoint.getLatitude(), previousTrackpoint.getLongitude(), type, speed));
|
||||
}
|
||||
}
|
||||
}
|
||||
lastTrackPoint = null;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Couldn't read trackpoints from OpenTracks URI", e);
|
||||
}
|
||||
return new TrackPointsBySegments(segments);
|
||||
}
|
||||
|
||||
public long getTrackPointId() {
|
||||
return trackPointId;
|
||||
}
|
||||
|
||||
public long getTrackId() {
|
||||
return trackId;
|
||||
}
|
||||
|
||||
public double getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/* Copyright (C) 2025 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.DoubleStream;
|
||||
|
||||
/**
|
||||
* This class was copied and modified from
|
||||
* https://github.com/OpenTracksApp/OSMDashboard/blob/v4.3.0/src/main/java/de/storchp/opentracks/osmplugin/dashboardapi/TrackPointsBySegments.java
|
||||
*/
|
||||
public record TrackPointsBySegments(List<List<TrackPoint>> segments) {
|
||||
|
||||
public boolean isEmpty() {
|
||||
return segments.isEmpty();
|
||||
}
|
||||
|
||||
public double calcAverageSpeed() {
|
||||
return streamTrackPointsWithSpeed().average().orElse(0.0);
|
||||
}
|
||||
|
||||
public double calcMaxSpeed() {
|
||||
return streamTrackPointsWithSpeed().max().orElse(0.0);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private DoubleStream streamTrackPointsWithSpeed() {
|
||||
return segments.stream().flatMap(List::stream).mapToDouble(TrackPoint::getSpeed).filter(speed -> speed > 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user