diff --git a/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java b/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java index bef2a78b..ca94dcd0 100644 --- a/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java +++ b/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java @@ -11,6 +11,8 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.Matchers.stringContainsInOrder; +import android.util.Log; + import android.content.Context; import android.content.SharedPreferences; @@ -62,6 +64,7 @@ public void tearDown() { */ @Test public void testStorageDirectoryValidatesNonEmpty() { + Log.i("More", "magic"); String keyTitle = context.getString(R.string.prefs_storage_dir); String defaultValue = OSMTracker.Preferences.VAL_STORAGE_DIR; diff --git a/app/src/androidTest/java/net/osmtracker/layouts/DownloadLayoutTest.java b/app/src/androidTest/java/net/osmtracker/layouts/DownloadLayoutTest.java index 5d7c99d2..a1d372d5 100644 --- a/app/src/androidTest/java/net/osmtracker/layouts/DownloadLayoutTest.java +++ b/app/src/androidTest/java/net/osmtracker/layouts/DownloadLayoutTest.java @@ -153,4 +153,4 @@ private void clickButtonsToDownloadLayout(String layoutName) { TestUtils.checkToastIsShownWith(TestUtils.getStringResource(R.string.available_layouts_successful_download)); } -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 071c23b8..f66ff13c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,11 @@ + + { @@ -36,10 +39,15 @@ public TracklistAdapter getCursorAdapter() { } public interface TrackListRecyclerViewAdapterListener { - void onClick(long trackId); + default void onClick(long trackId) {}; void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo, long trackId); + default void onClick(TrackItemVH item, long trackId) { + onClick(trackId); + } + default void initializeItem(TrackItemVH item, long trackId) { + } } /** @@ -104,9 +112,23 @@ public ImageView getvUploadStatus() { @Override public void onClick(View v) { long trackId = Long.parseLong(getvId().getText().toString()); - mHandler.onClick(trackId); + mHandler.onClick(this, trackId); } + public void activate(boolean state) { + if(state) + itemView.setBackgroundColor(Color.RED); + else { + TypedValue outValue = new TypedValue(); + itemView + .getContext() + .getTheme() + .resolveAttribute(android.R.attr.selectableItemBackground, + outValue, true); + itemView.setBackgroundResource(outValue.resourceId); + } + } + @Override public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) { long trackId = Long.parseLong(getvId().getText().toString()); @@ -130,8 +152,11 @@ public void onBindViewHolder(@NonNull TrackItemVH holder, int position) { // contents of the view with that element // Passing the binding operation to cursor loader - cursorAdapter.getCursor().moveToPosition(position); - cursorAdapter.bindView(holder.itemView, context, cursorAdapter.getCursor()); + Cursor cursor = cursorAdapter.getCursor(); + cursor.moveToPosition(position); + cursorAdapter.bindView(holder.itemView, context, cursor); + mHandler.initializeItem(holder, + cursor.getLong(cursor.getColumnIndex(TrackContentProvider.Schema.COL_ID))); } @Override diff --git a/app/src/main/java/net/osmtracker/activity/TrackPicker.java b/app/src/main/java/net/osmtracker/activity/TrackPicker.java new file mode 100644 index 00000000..6274a90b --- /dev/null +++ b/app/src/main/java/net/osmtracker/activity/TrackPicker.java @@ -0,0 +1,151 @@ +package net.osmtracker.activity; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.View; + +import net.osmtracker.OSMTracker; +import net.osmtracker.R; +import net.osmtracker.db.TrackContentProvider; +import net.osmtracker.activity.TrackListRVAdapter.TrackItemVH; + +/** + * Lists existing tracks. Each track is displayed using {@link RecyclerView} + * + * @author Alain Knaff + */ +public class TrackPicker extends AppCompatActivity + implements TrackListRVAdapter.TrackListRecyclerViewAdapterListener +{ + private static final String TAG = TrackPicker.class.getSimpleName(); + + /** Constant used if no track is active (-1)*/ + private static final long TRACK_ID_NO_TRACK = -1; + + // The active track being recorded, if any, or {TRACK_ID_NO_TRACK}; + // value is updated in {@link #onResume()} + private long currentTrackId = TRACK_ID_NO_TRACK; + + private TrackListRVAdapter recyclerViewAdapter; + + private RecyclerView recyclerView; + + + // To check if the RecyclerView already has a + // DividerItemDecoration added + private boolean hasDivider; + + private long overlaidTrackId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.trackpicker); + + // Toolbar myToolbar = findViewById(R.id.my_toolbar); + // setSupportActionBar(myToolbar); + + recyclerView = findViewById(R.id.recyclerview); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + + // Adding a horizontal divider + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL); + dividerItemDecoration.setDrawable(ContextCompat.getDrawable(this, R.drawable.divider)); // Using a custom drawable + + recyclerView.addItemDecoration(dividerItemDecoration); + } + + @Override + protected void onResume() { + activatedItem = null; + Intent i = getIntent(); + overlaidTrackId = i.getLongExtra(OSMTracker.INTENT_OVERLAID_TRACK_ID, 0); + currentTrackId = i.getLongExtra(TrackContentProvider.Schema.COL_TRACK_ID, 0); + setRecyclerView(); + super.onResume(); + } + + + /** + * Configures and initializes the RecyclerView for displaying the list of tracks. + */ + private void setRecyclerView() { + RecyclerView recyclerView = findViewById(R.id.recyclerview); + + LinearLayoutManager layoutManager = new LinearLayoutManager(this, + LinearLayoutManager.VERTICAL, false); + recyclerView.setLayoutManager(layoutManager); + // adds a divider decoration if not already present + if (!hasDivider) { + DividerItemDecoration did = new DividerItemDecoration(recyclerView.getContext(), + layoutManager.getOrientation()); + recyclerView.addItemDecoration(did); + hasDivider = true; + } + recyclerView.setHasFixedSize(true); + Cursor cursor = getContentResolver().query( + TrackContentProvider.CONTENT_URI_TRACK, null, null, null, + TrackContentProvider.Schema.COL_START_DATE + " desc"); + + recyclerViewAdapter = new TrackListRVAdapter(this, cursor, this); + recyclerView.setAdapter(recyclerViewAdapter); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo, long trackId) { + super.onCreateContextMenu(menu, v, menuInfo); + } + + private TrackItemVH activatedItem = null; + + @Override + public void onClick(TrackItemVH item, long trackId) { + Intent i; + + if(trackId == this.currentTrackId) + // ignore clicks on current track + return; + + overlaidTrackId = select(item, trackId); + Intent data = new Intent(); + data.putExtra(OSMTracker.INTENT_OVERLAID_TRACK_ID, + overlaidTrackId); + setResult(RESULT_OK, data); + finish(); + } + + private long select(TrackItemVH item, long trackId) { + if(activatedItem != null) + // if a track was activated, whether same or another, + // deactivate + activatedItem.activate(false); + + if(trackId == this.overlaidTrackId) + // if previously activated track was clicked, this + // means user doesn't want any track to be activated + return 0; + + item.activate(true); + return trackId; + } + + @Override + public void initializeItem(TrackItemVH item, long trackId) { + if(trackId == this.currentTrackId) + // prevent user from clicking on same track + item.itemView.setEnabled(false); + if(trackId == this.overlaidTrackId) { + item.activate(true); + activatedItem = item; + } + } +} diff --git a/app/src/main/java/net/osmtracker/db/DataHelper.java b/app/src/main/java/net/osmtracker/db/DataHelper.java index 410e8546..79d30623 100644 --- a/app/src/main/java/net/osmtracker/db/DataHelper.java +++ b/app/src/main/java/net/osmtracker/db/DataHelper.java @@ -384,6 +384,46 @@ public void deleteNote(String uuid) { } } + /** + * Updates overlaid track of trackId + * + * @param trackId Id of the track + * @param overlay id of the track + */ + public void updateOverlay(long trackId, long overlay) { + Log.v(TAG, "Changing overlay track=" + trackId + + ", overlay=" + overlay); + + // delete old + contentResolver.delete(ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_OVERLAY, trackId), null, null); + + // insert new + if(overlay != 0) { + ContentValues values = new ContentValues(); + values.put(TrackContentProvider.Schema.COL_TRACK_ID, trackId); + values.put(TrackContentProvider.Schema.COL_OVERLAY_ID, overlay); + contentResolver.insert(TrackContentProvider.CONTENT_URI_OVERLAY, values); + } + } + + /** + * Find the ID for track overlaying trackId + * @param trackId Id of the track + * @param cr {@link ContentResolver} for query + * @return the track ID for the overlaid track, or 0 if not found + */ + public static long queryOverlay(long trackId, @NotNull ContentResolver cr) { + Cursor ca = cr.query(ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_OVERLAY, + trackId),null, null, null, null); + + long overlayTrackId = 0; + if (ca.moveToFirst()) + overlayTrackId = ca.getLong(ca.getColumnIndex(TrackContentProvider.Schema.COL_OVERLAY_ID)); + ca.close(); + return overlayTrackId; + } + + /** * Stop tracking by making the track inactive * @param trackId Id of the track diff --git a/app/src/main/java/net/osmtracker/db/DatabaseHelper.java b/app/src/main/java/net/osmtracker/db/DatabaseHelper.java index a106badd..23bbf93b 100644 --- a/app/src/main/java/net/osmtracker/db/DatabaseHelper.java +++ b/app/src/main/java/net/osmtracker/db/DatabaseHelper.java @@ -116,6 +116,16 @@ public class DatabaseHelper extends SQLiteOpenHelper { + TrackContentProvider.Schema.COL_OSM_UPLOAD_DATE + " long" // null indicates not yet uploaded + ")"; + /** + * SQL for creating table OVERLAY + * @since 20 + */ + private static final String SQL_CREATE_TABLE_OVERLAY = "" + + "create table " + TrackContentProvider.Schema.TBL_OVERLAY + " (" + + TrackContentProvider.Schema.COL_TRACK_ID + " integer primary key," + + TrackContentProvider.Schema.COL_OVERLAY_ID + " integer not null" + + ")"; + /** * Database name. */ @@ -145,7 +155,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { * v19: add TBL_TRACKPOINT.COL_SEG_ID for track segments support * */ - private static final int DB_VERSION = 19; + private static final int DB_VERSION = 20; private Context context; @@ -207,6 +217,8 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(SQL_CREATE_TABLE_NOTE); case 18: db.execSQL("alter table "+TrackContentProvider.Schema.TBL_TRACKPOINT + " add column " + TrackContentProvider.Schema.COL_SEG_ID + " integer default 0"); + case 19: + db.execSQL(SQL_CREATE_TABLE_OVERLAY); } } diff --git a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java index 31f65ab9..9f75721b 100644 --- a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java +++ b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java @@ -38,6 +38,8 @@ public class TrackContentProvider extends ContentProvider { public static final Uri CONTENT_URI_NOTE = Uri.parse("content://" + AUTHORITY + "/" + Schema.TBL_NOTE); + public static final Uri CONTENT_URI_OVERLAY = Uri.parse("content://" + AUTHORITY + "/" + Schema.TBL_OVERLAY); + /** * Uri for the active track */ @@ -125,6 +127,8 @@ public class TrackContentProvider extends ContentProvider { uriMatcher.addURI(AUTHORITY, Schema.TBL_TRACKPOINT + "/#", Schema.URI_CODE_TRACKPOINT_ID); uriMatcher.addURI(AUTHORITY, Schema.TBL_NOTE + "/#", Schema.URI_CODE_NOTE_ID); uriMatcher.addURI(AUTHORITY, Schema.TBL_NOTE + "/uuid/*", Schema.URI_CODE_NOTE_UUID); + uriMatcher.addURI(AUTHORITY, Schema.TBL_OVERLAY, Schema.URI_CODE_OVERLAYS); + uriMatcher.addURI(AUTHORITY, Schema.TBL_OVERLAY + "/#", Schema.URI_CODE_OVERLAY_ID); } /** @@ -227,6 +231,8 @@ public int delete(Uri uri, String selection, String[] selectionArgs) { String trackId = Long.toString(ContentUris.parseId(uri)); dbHelper.getWritableDatabase().delete(Schema.TBL_WAYPOINT, Schema.COL_TRACK_ID + " = ?", new String[] {trackId}); dbHelper.getWritableDatabase().delete(Schema.TBL_TRACKPOINT, Schema.COL_TRACK_ID + " = ?", new String[] {trackId}); + dbHelper.getWritableDatabase().delete(Schema.TBL_OVERLAY, Schema.COL_TRACK_ID + " = ?", new String[] {trackId}); + dbHelper.getWritableDatabase().delete(Schema.TBL_OVERLAY, Schema.COL_OVERLAY_ID + " = ?", new String[] {trackId}); count = dbHelper.getWritableDatabase().delete(Schema.TBL_TRACK, Schema.COL_ID + " = ?", new String[] {trackId}); break; case Schema.URI_CODE_WAYPOINT_UUID: @@ -245,6 +251,10 @@ public int delete(Uri uri, String selection, String[] selectionArgs) { count = 0; } break; + case Schema.URI_CODE_OVERLAY_ID: + String oTrackId = Long.toString(ContentUris.parseId(uri)); + count = dbHelper.getWritableDatabase().delete(Schema.TBL_OVERLAY, Schema.COL_TRACK_ID + " = ?", new String[] {oTrackId}); + break; default: throw new IllegalArgumentException("Unknown URI: " + uri); } @@ -345,6 +355,19 @@ public Uri insert(Uri uri, ContentValues values) { throw new IllegalArgumentException("values should provide " + Schema.COL_START_DATE); } break; + case Schema.URI_CODE_OVERLAYS: + if (values.containsKey(Schema.COL_TRACK_ID) && + values.containsKey(Schema.COL_OVERLAY_ID)) { + long rowId = dbHelper.getWritableDatabase().insert(Schema.TBL_OVERLAY, null, values); + if (rowId > 0) { + Uri overlayUri = ContentUris.withAppendedId(CONTENT_URI_OVERLAY, rowId); + getContext().getContentResolver().notifyChange(overlayUri, null); + return overlayUri; + } + } else { + throw new IllegalArgumentException("values should provide " + Schema.COL_TRACK_ID+ " and "+Schema.COL_OVERLAY_ID); + } + break; default: throw new IllegalArgumentException("Unknown URI: " + uri); } @@ -490,6 +513,16 @@ public Cursor query(Uri uri, String[] projection, String selectionIn, String[] s selection = Schema.TBL_TRACKPOINT + "." + Schema.COL_ID + " = ?"; selectionArgs = new String[] {trackPointId}; break; + case Schema.URI_CODE_OVERLAY_ID: + if (selectionIn != null || selectionArgsIn != null) { + // Any selection/selectionArgs will be ignored + throw new UnsupportedOperationException(); + } + trackId = uri.getLastPathSegment(); + qb.setTables(Schema.TBL_OVERLAY); + selection = Schema.TBL_OVERLAY + "." + Schema.COL_TRACK_ID + " = ?"; + selectionArgs = new String[] {trackId}; + break; default: throw new IllegalArgumentException("Unknown URI: " + uri); } @@ -574,6 +607,7 @@ public static final class Schema { public static final String TBL_WAYPOINT = "waypoint"; public static final String TBL_NOTE = "note"; public static final String TBL_TRACK = "track"; + public static final String TBL_OVERLAY = "overlay"; public static final String COL_ID = "_id"; public static final String COL_TRACK_ID = "track_id"; public static final String COL_UUID = "uuid"; @@ -599,6 +633,7 @@ public static final class Schema { 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"; + public static final String COL_OVERLAY_ID = "overlay_id"; // virtual colums that are used in some sqls but dont exist in database public static final String COL_TRACKPOINT_COUNT = "tp_count"; @@ -620,6 +655,8 @@ public static final class Schema { public static final int URI_CODE_TRACK_NOTES = 13; public static final int URI_CODE_NOTE_ID = 14; public static final int URI_CODE_NOTE_UUID = 15; + public static final int URI_CODE_OVERLAYS = 16; + public static final int URI_CODE_OVERLAY_ID = 17; public static final int VAL_TRACK_ACTIVE = 1; diff --git a/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java b/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java index 0d20d0b6..2b92d82d 100644 --- a/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java +++ b/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java @@ -351,7 +351,6 @@ protected void writeTrackPoints(String trackName, Writer fw, Cursor c, boolean f 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"); @@ -635,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/service/gps/GPSLogger.java b/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java index 901cc5e6..f57b4132 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; @@ -109,6 +114,8 @@ public class GPSLogger extends Service implements LocationListener { */ private PressureListener pressureListener = new PressureListener(); + private boolean newSeg = false; + /** * Receives Intent for way point and notes tracking, and stop/start logging. */ @@ -191,6 +198,7 @@ public void onReceive(Context context, Intent intent) { dataHelper.updateNote(trackId, uuid, name); } } else if (OSMTracker.INTENT_START_TRACKING.equals(intent.getAction())) { + newSeg = true; Bundle extras = intent.getExtras(); if (extras != null) { Long trackId = extras.getLong(TrackContentProvider.Schema.COL_TRACK_ID); @@ -318,6 +326,23 @@ 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. */ diff --git a/app/src/main/res/layout/trackpicker.xml b/app/src/main/res/layout/trackpicker.xml new file mode 100644 index 00000000..108127f6 --- /dev/null +++ b/app/src/main/res/layout/trackpicker.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/displaytrackmap_menu.xml b/app/src/main/res/menu/displaytrackmap_menu.xml index 8ee7003a..eb00f088 100644 --- a/app/src/main/res/menu/displaytrackmap_menu.xml +++ b/app/src/main/res/menu/displaytrackmap_menu.xml @@ -5,6 +5,9 @@ android:id="@+id/displaytrackmap_menu_center_to_gps" android:icon="@android:drawable/ic_menu_mylocation" android:title="@string/menu_center_to_gps" /> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 277fe80e..2db92cc4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,6 +52,8 @@ Unable to export track: {0} All tracks will be exported, which could take a long time. Are you sure? Unable to process the track: {0} + + List of tracks to overlay: Track Details Start time: @@ -133,6 +135,7 @@ Export as GPX OpenStreetMap upload Center to GPS + Overlay other Export all as GPX Unable to write to external storage.