diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..5be040e03 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms +# github: [labexp] +liberapay: OSMTracker diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index c139e76e9..d1505ca00 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -51,12 +51,34 @@ jobs: distribution: 'temurin' java-version: '17' + - name: Setup Signing Keystore + run: | + # 1. Decode nightly keystore secret + echo "${{ secrets.NIGHTLY_KEYSTORE }}" | base64 -d > /tmp/nightly-keystore.jks + echo "Keystore created in /tmp/nightly-keystore.jks" + + # Verify + echo "✅ Keystore created in: /tmp/nightly-keystore.jks" + ls -la /tmp/nightly-keystore.jks + echo "ks_path=/tmp/nightly-keystore.jks" >> $GITHUB_OUTPUT + - name: Setup Android SDK uses: android-actions/setup-android@v2 - name: Build with Gradle - run: ./gradlew assembleDebug --stacktrace + run: | + ./gradlew assembleDebug --stacktrace \ + -Pandroid.injected.signing.store.file="/tmp/nightly-keystore.jks" \ + -Pandroid.injected.signing.store.password="${{ secrets.KEYSTORE_PASSWORD }}" \ + -Pandroid.injected.signing.key.alias="${{ secrets.KEY_ALIAS }}" \ + -Pandroid.injected.signing.key.password="${{ secrets.KEY_PASSWORD }}" + + - name: Cleanup Keystore + run: | + echo "Cleaning keystore..." + rm -f app/nightly-keystore.jks + echo "✅ Keystore removed" - name: Rename output APK run: | diff --git a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java index 22720fbbc..2d178e419 100644 --- a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java +++ b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java @@ -23,6 +23,7 @@ import net.osmtracker.R; import net.osmtracker.db.TrackContentProvider; import net.osmtracker.overlay.WayPointsOverlay; +import net.osmtracker.overlay.Polylines; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; @@ -33,7 +34,6 @@ import org.osmdroid.util.GeoPoint; import org.osmdroid.views.CustomZoomButtonsController; import org.osmdroid.views.MapView; -import org.osmdroid.views.overlay.Polyline; import org.osmdroid.views.overlay.ScaleBarOverlay; import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay; @@ -118,7 +118,7 @@ public class DisplayTrackMap extends Activity { /** * OSM view overlay that displays current path */ - private Polyline polyline; + private Polylines polylines; /** * OSM view overlay that displays waypoints @@ -158,6 +158,11 @@ public class DisplayTrackMap extends Activity { */ private Integer lastTrackPointIdProcessed = null; + /** + * The id of the last segment + */ + private int prevSegmentId=-1; + /** * Observes changes on track points */ @@ -303,6 +308,7 @@ private void resumeActivity() { // This ensures that all waypoints for the track will be reloaded // from the database to populate the path layout lastTrackPointIdProcessed = null; + prevSegmentId = -1; // Reload path pathChanged(); @@ -321,7 +327,7 @@ protected void onPause() { getContentResolver().unregisterContentObserver(trackpointContentObserver); // Clear the points list. - polyline.setPoints(new ArrayList<>()); + polylines.clear(); super.onPause(); } @@ -387,12 +393,8 @@ private void createOverlays() { this.getWindowManager().getDefaultDisplay().getMetrics(metrics); // set with to hopefully DPI independent 0.5mm - polyline = new Polyline(); - Paint paint = polyline.getOutlinePaint(); - paint.setColor(Color.BLUE); - paint.setStrokeWidth((float) (metrics.densityDpi / 25.4 / 2)); - osmView.getOverlayManager().add(polyline); - + polylines = new Polylines(Color.BLUE, (float)(metrics.densityDpi / 25.4 / 2), osmView); + myLocationOverlay = new SimpleLocationOverlay(this); osmView.getOverlays().add(myLocationOverlay); @@ -439,7 +441,7 @@ private void pathChanged() { // Projection: The columns to retrieve. Here, we want the latitude, // longitude and primary key only - String[] projection = {TrackContentProvider.Schema.COL_LATITUDE, TrackContentProvider.Schema.COL_LONGITUDE, TrackContentProvider.Schema.COL_ID}; + String[] projection = {TrackContentProvider.Schema.COL_LATITUDE, TrackContentProvider.Schema.COL_LONGITUDE, TrackContentProvider.Schema.COL_ID, TrackContentProvider.Schema.COL_SEG_ID }; // Selection: The where clause to use String selection = null; // SelectionArgs: The parameter replacements to use for the '?' in the selection @@ -470,13 +472,20 @@ private void pathChanged() { int primaryKeyColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_ID); int latitudeColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE); int longitudeColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE); + int segmentIdColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_SEG_ID); // Add each new point to the track while (!c.isAfterLast()) { lastLat = c.getDouble(latitudeColumnIndex); lastLon = c.getDouble(longitudeColumnIndex); lastTrackPointIdProcessed = c.getInt(primaryKeyColumnIndex); - polyline.addPoint(new GeoPoint(lastLat, lastLon)); + int segmentId = c.getInt(segmentIdColumnIndex); + if(segmentId != prevSegmentId) { + polylines.nextSegment(); + } + prevSegmentId = segmentId; + + polylines.addPoint(new GeoPoint(lastLat, lastLon)); if (doInitialBoundsCalc) { if (lastLat < minLat) minLat = lastLat; if (lastLon < minLon) minLon = lastLon; diff --git a/app/src/main/java/net/osmtracker/db/DataHelper.java b/app/src/main/java/net/osmtracker/db/DataHelper.java index 37935bec1..8875c430e 100644 --- a/app/src/main/java/net/osmtracker/db/DataHelper.java +++ b/app/src/main/java/net/osmtracker/db/DataHelper.java @@ -146,7 +146,7 @@ public DataHelper(Context c) { * @param pressure * atmospheric pressure */ - public void track(long trackId, Location location, float azimuth, int accuracy, float pressure) { + public void track(long trackId, Location location, float azimuth, int accuracy, float pressure, long segId) { Log.v(TAG, "Tracking (trackId=" + trackId + ") location: " + location + " azimuth: " + azimuth + ", accuracy: " + accuracy); ContentValues values = new ContentValues(); values.put(TrackContentProvider.Schema.COL_TRACK_ID, trackId); @@ -180,6 +180,8 @@ public void track(long trackId, Location location, float azimuth, int accuracy, values.put(TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE, pressure); } + values.put(TrackContentProvider.Schema.COL_SEG_ID, segId); + Uri trackUri = ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, trackId); contentResolver.insert(Uri.withAppendedPath(trackUri, TrackContentProvider.Schema.TBL_TRACKPOINT + "s"), values); } diff --git a/app/src/main/java/net/osmtracker/db/DatabaseHelper.java b/app/src/main/java/net/osmtracker/db/DatabaseHelper.java index fda2032c5..a5600a713 100644 --- a/app/src/main/java/net/osmtracker/db/DatabaseHelper.java +++ b/app/src/main/java/net/osmtracker/db/DatabaseHelper.java @@ -39,7 +39,9 @@ public class DatabaseHelper extends SQLiteOpenHelper { + TrackContentProvider.Schema.COL_TIMESTAMP + " long not null," + TrackContentProvider.Schema.COL_COMPASS + " double null," + TrackContentProvider.Schema.COL_COMPASS_ACCURACY + " integer null," - + TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE + " double null" + ")"; + + TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE + " double null," + + TrackContentProvider.Schema.COL_SEG_ID + " integer not null default 0" + + ")"; /** * SQL for creating index TRACKPOINT_idx (track id) @@ -201,6 +203,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("alter table " + TrackContentProvider.Schema.TBL_TRACKPOINT + " add column " + TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE + " double null"); db.execSQL("alter table " + TrackContentProvider.Schema.TBL_WAYPOINT + " add column " + TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE + " double null"); case 17: + db.execSQL("alter table "+TrackContentProvider.Schema.TBL_TRACKPOINT + " add column " + TrackContentProvider.Schema.COL_SEG_ID + " integer default 0"); db.execSQL(SQL_CREATE_TABLE_NOTE); } } diff --git a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java index a5129c8a4..45732c763 100644 --- a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java +++ b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java @@ -83,6 +83,7 @@ public class TrackContentProvider extends ContentProvider { Schema.COL_OSM_VISIBILITY, Schema.COL_START_DATE, "count(" + Schema.TBL_TRACKPOINT + "." + Schema.COL_ID + ") as " + Schema.COL_TRACKPOINT_COUNT, + "(SELECT max("+Schema.TBL_TRACKPOINT+"."+Schema.COL_SEG_ID+") FROM "+Schema.TBL_TRACKPOINT+" WHERE "+Schema.TBL_TRACKPOINT+"."+Schema.COL_TRACK_ID+" = " + Schema.TBL_TRACK + "." + Schema.COL_ID + ") as " + Schema.COL_MAX_SEG_ID, "(SELECT count(" + Schema.TBL_WAYPOINT + "." + Schema.COL_TRACK_ID +") " + "FROM " + Schema.TBL_WAYPOINT + " " + "WHERE " + Schema.TBL_WAYPOINT + "." + Schema.COL_TRACK_ID +" " + @@ -593,10 +594,14 @@ public static final class Schema { public static final String COL_COMPASS = "compass_heading"; public static final String COL_COMPASS_ACCURACY = "compass_accuracy"; public static final String COL_ATMOSPHERIC_PRESSURE = "atmospheric_pressure"; - + + public static final String COL_SEG_ID = "segment_id"; + // virtual colums that are used in some sqls but dont exist in database public static final String COL_TRACKPOINT_COUNT = "tp_count"; public static final String COL_WAYPOINT_COUNT = "wp_count"; + public static final String COL_MAX_SEG_ID = "max_segment_id"; + public static final String COL_NOTE_COUNT = "note_count"; // Codes for UriMatcher diff --git a/app/src/main/java/net/osmtracker/db/model/Track.java b/app/src/main/java/net/osmtracker/db/model/Track.java index 03261b482..4c3798d9c 100644 --- a/app/src/main/java/net/osmtracker/db/model/Track.java +++ b/app/src/main/java/net/osmtracker/db/model/Track.java @@ -51,7 +51,7 @@ public static OSMVisibility fromPosition(int position) { private String description; private OSMVisibility visibility; private List tags = new ArrayList(); - private int tpCount, wpCount, noteCount; + private int tpCount, wpCount, noteCount, maxSegId; private long trackDate; private long trackId; @@ -92,6 +92,9 @@ public static Track build(final long trackId, Cursor tc, ContentResolver cr, boo out.tpCount = tc.getInt(tc.getColumnIndex(TrackContentProvider.Schema.COL_TRACKPOINT_COUNT)); out.wpCount = tc.getInt(tc.getColumnIndex(TrackContentProvider.Schema.COL_WAYPOINT_COUNT)); + + int maxSegIdIdx = tc.getColumnIndex(TrackContentProvider.Schema.COL_MAX_SEG_ID); + out.maxSegId = tc.isNull(maxSegIdIdx) ? 0 :tc.getInt(maxSegIdIdx); out.noteCount = tc.getInt(tc.getColumnIndex(TrackContentProvider.Schema.COL_NOTE_COUNT)); @@ -147,9 +150,12 @@ public void setWpCount(int wpCount) { this.wpCount = wpCount; } + public void setMaxSegId(int maxSegId) { + this.maxSegId = maxSegId; + } public void setNoteCount(int noteCount) { this.noteCount = noteCount; - } + } public void setTracktDate(long tracktDate) { this.trackDate = tracktDate; @@ -193,6 +199,10 @@ public Integer getWpCount() { return wpCount; } + public Integer getMaxSegId() { + return maxSegId; + } + public Integer getTpCount() { return tpCount; } diff --git a/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java b/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java index c54faa2a3..662cee106 100644 --- a/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java +++ b/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java @@ -348,8 +348,16 @@ private void writeTrackPoints(String trackName, Writer fw, Cursor c, boolean fil fw.write("\t\t" + "" + "\n"); int i=0; + int prevSegId=-1; for(c.moveToFirst(); !c.isAfterLast(); c.moveToNext(),i++) { StringBuffer out = new StringBuffer(); + int segId = c.getInt(c.getColumnIndex(TrackContentProvider.Schema.COL_SEG_ID)); + if(prevSegId != -1 && segId != prevSegId) { + fw.write("\t\t" + "" + "\n"); + fw.write("\t\t" + "" + "\n"); + } + prevSegId = segId; + out.append("\t\t\t" + "" + "\n"); @@ -626,4 +634,4 @@ public String sanitizeTrackName(String trackName){ public String getErrorMsg() { return errorMsg; } -} \ No newline at end of file +} diff --git a/app/src/main/java/net/osmtracker/overlay/Polylines.java b/app/src/main/java/net/osmtracker/overlay/Polylines.java new file mode 100644 index 000000000..d695d2324 --- /dev/null +++ b/app/src/main/java/net/osmtracker/overlay/Polylines.java @@ -0,0 +1,61 @@ +package net.osmtracker.overlay; + +import java.util.ArrayList; +import java.util.List; + +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.Polyline; + +import android.graphics.Paint; + +/** + * Collection of Polylines, useful to draw interrupted paths + */ +public class Polylines { + private int color; + private float width; + private MapView osmView; + private boolean havePoint; + + private int curIdx=0; + + private List polylines = new ArrayList(); + + private void addPolyline() { + Polyline polyline = new Polyline(); + Paint paint = polyline.getOutlinePaint(); + paint.setColor(color); + paint.setStrokeWidth(width); + + polylines.add(polyline); + osmView.getOverlayManager().add(polyline); + } + + public void clear() { + for(Polyline polyline : polylines) + polyline.setPoints(new ArrayList<>()); + curIdx=0; + } + + public Polylines(int color, float width, MapView osmView) { + this.color=color; + this.width=width; + this.osmView = osmView; + addPolyline(); + havePoint=false; + } + + public void addPoint(GeoPoint gp) { + if(curIdx >= polylines.size()) + addPolyline(); + polylines.get(curIdx).addPoint(gp); + havePoint=true; + } + + public void nextSegment() { + if(havePoint) + curIdx++; + havePoint=false; + } +} diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java b/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java index 7f180541a..934d13209 100644 --- a/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java +++ b/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java @@ -6,6 +6,8 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.ContentResolver; +import android.content.ContentUris; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -24,11 +26,14 @@ import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; +import android.database.Cursor; + import net.osmtracker.OSMTracker; import net.osmtracker.R; import net.osmtracker.activity.TrackLogger; import net.osmtracker.db.DataHelper; import net.osmtracker.db.TrackContentProvider; +import net.osmtracker.db.model.Track; import net.osmtracker.listener.PressureListener; import net.osmtracker.listener.SensorListener; @@ -83,6 +88,11 @@ public class GPSLogger extends Service implements LocationListener { */ private long currentTrackId = -1; + /** + * Current Segment ID + */ + private long currentSegmentId = -1; + /** * the timestamp of the last GPS fix we used */ @@ -130,7 +140,7 @@ public void onReceive(Context context, Intent intent) { dataHelper.wayPoint(trackId, lastLocation, name, link, uuid, sensorListener.getAzimuth(), sensorListener.getAccuracy(), pressureListener.getPressure()); // If there is a waypoint in the track, there should also be a trackpoint - dataHelper.track(currentTrackId, lastLocation, sensorListener.getAzimuth(), sensorListener.getAccuracy(), pressureListener.getPressure()); + dataHelper.track(currentTrackId, lastLocation, sensorListener.getAzimuth(), sensorListener.getAccuracy(), pressureListener.getPressure(), currentSegmentId); } } } @@ -313,12 +323,31 @@ public void onDestroy() { super.onDestroy(); } + private long getSegIdFor(long trackId) { + ContentResolver cr = getContentResolver(); + try(Cursor cursor = + cr.query(ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, trackId), + null, null, null, null)) { + + if (! cursor.moveToFirst()) { + Log.v(TAG, "Track "+trackId+" not found"); + return 0; // <--- Early return --- + } + + return Track + .build(trackId, cursor, cr, true) + .getMaxSegId(); + } + } + /** * Start GPS tracking. */ private void startTracking(long trackId) { currentTrackId = trackId; - Log.v(TAG, "Starting track logging for track #" + trackId); + currentSegmentId = getSegIdFor(trackId)+1; + Log.v(TAG, "Starting track logging for track #" + trackId + + "/" + currentSegmentId); // Refresh notification with correct Track ID NotificationManager nmgr = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nmgr.notify(NOTIFICATION_ID, getNotification()); @@ -347,7 +376,7 @@ public void onLocationChanged(Location location) { lastLocation = location; if (isTracking) { - dataHelper.track(currentTrackId, location, sensorListener.getAzimuth(), sensorListener.getAccuracy(), pressureListener.getPressure()); + dataHelper.track(currentTrackId, location, sensorListener.getAzimuth(), sensorListener.getAccuracy(), pressureListener.getPressure(), currentSegmentId); } } }