diff --git a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java index f6ef7a8dd..bb5924bfc 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java @@ -19,7 +19,6 @@ package de.dennisguse.opentracks; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.database.Cursor; import android.graphics.drawable.AnimatedVectorDrawable; import android.location.LocationManager; import android.os.Bundle; @@ -46,6 +45,7 @@ import java.util.Arrays; import java.util.Objects; import de.dennisguse.opentracks.data.ContentProviderUtils; +import de.dennisguse.opentracks.data.TrackListIterator; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.databinding.TrackListBinding; import de.dennisguse.opentracks.sensors.GpsStatusValue; @@ -63,6 +63,7 @@ import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragm import de.dennisguse.opentracks.ui.markers.MarkerListActivity; import de.dennisguse.opentracks.ui.util.ActivityUtils; import de.dennisguse.opentracks.ui.util.RecyclerViewSwipeDeleteCallback; +import de.dennisguse.opentracks.util.AutoCloseableIterator; import de.dennisguse.opentracks.util.IntentDashboardUtils; import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.PermissionRequester; @@ -311,7 +312,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co viewBinding.trackListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.app_name))); - Cursor tracks = new ContentProviderUtils(this).searchTracks(searchQuery); + AutoCloseableIterator tracks = new ContentProviderUtils(this).searchTracks(searchQuery); adapter.swapData(tracks); } diff --git a/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java index 1d23af37f..049430552 100644 --- a/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java @@ -180,14 +180,12 @@ public class ContentProviderUtils { return tracks; } - //TODO Implement someting like TrackPointIterator: just as a cursor - public Cursor searchTracks(String searchQuery) { + public AutoCloseableIterator searchTracks(String searchQuery) { // Needed, because MARKER_COUNT is a virtual column and has to be explicitly requested. - // Used only be TrackListAdapter final String[] PROJECTION = new String[]{ TracksColumns._ID, TracksColumns.NAME, - TracksColumns.DESCRIPTION, //TODO Needed? + TracksColumns.DESCRIPTION, TracksColumns.ACTIVITY_TYPE, TracksColumns.ACTIVITY_TYPE_LOCALIZED, TracksColumns.STARTTIME, @@ -208,7 +206,7 @@ public class ContentProviderUtils { selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"}; } - return contentResolver.query(TracksColumns.CONTENT_URI, PROJECTION, selection, selectionArgs, sortOrder); + return new TrackListIterator(contentResolver.query(TracksColumns.CONTENT_URI, PROJECTION, selection, selectionArgs, sortOrder)); } public Track getTrack(@NonNull Track.Id trackId) { @@ -508,6 +506,7 @@ public class ContentProviderUtils { return contentResolver.query(MarkerColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder); } + //TODO Use AutoCloseableIterator public List searchMarkers(Track.Id trackId, String query) { String selection = null; String[] selectionArgs = null; diff --git a/src/main/java/de/dennisguse/opentracks/data/TrackListIterator.java b/src/main/java/de/dennisguse/opentracks/data/TrackListIterator.java new file mode 100644 index 000000000..07ee1971a --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/data/TrackListIterator.java @@ -0,0 +1,134 @@ +package de.dennisguse.opentracks.data; + +import android.database.Cursor; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.NoSuchElementException; + +import de.dennisguse.opentracks.data.models.ActivityType; +import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.data.tables.TracksColumns; +import de.dennisguse.opentracks.util.AutoCloseableIterator; + +public class TrackListIterator extends AutoCloseableIterator { + + private static final String TAG = TrackListIterator.class.getSimpleName(); + + private Cursor cursor; + + TrackListIterator(Cursor cursor) { + this.cursor = cursor; + } + + @NonNull + @Override + public Item get() { + final int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID); + final int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME); + final int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION); + final int activityTypeIndex = cursor.getColumnIndexOrThrow(TracksColumns.ACTIVITY_TYPE); + final int activityTypeLocalizedIndex = cursor.getColumnIndexOrThrow(TracksColumns.ACTIVITY_TYPE_LOCALIZED); + final int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME); + final int startTimeOffsetIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME_OFFSET); + final int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE); + final int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME); + final int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT); + + return new Item( + new Track.Id(cursor.getLong(idIndex)), + cursor.getString(nameIndex), + cursor.getString(descriptionIndex), + ActivityType.findBy(cursor.getString(activityTypeIndex)), + cursor.getString(activityTypeLocalizedIndex), + Instant.ofEpochMilli(cursor.getLong(startTimeIndex)), + ZoneOffset.ofTotalSeconds(cursor.getInt(startTimeOffsetIndex)), + Duration.ofMillis(cursor.getLong(totalTimeIndex)), + Distance.of(cursor.getFloat(totalDistanceIndex)), + cursor.getInt(markerCountIndex) + ); + } + + @Override + public boolean hasNext() { + if (cursor == null) { + return false; + } + return !cursor.isLast() && !cursor.isAfterLast(); + } + + @Override + @NonNull + public Item next() { + if (cursor == null || !cursor.moveToNext()) { + throw new NoSuchElementException(); + } + return get(); + } + + @Override + public int getPosition() { + return cursor.getPosition(); + } + + @Override + public boolean moveToPosition(int position) { + return cursor.moveToPosition(position); + } + + @VisibleForTesting + public int getCount() { + return cursor.getCount(); + } + + @Override + public void close() { + if (cursor != null) { + cursor.close(); + cursor = null; + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + public record Item( + @NonNull + Track.Id id, + + @NonNull + String name, + + @NonNull + String description, + + @NonNull + ActivityType activityType, + + @NonNull + String activityTypeLocalized, + + @NonNull + Instant startTime, + + @NonNull + ZoneOffset zoneOffset, + + @NonNull + Duration totalTime, + + @NonNull + Distance totalDistance, + + int markerCount + + ) { + } +} \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/ui/TrackListAdapter.java b/src/main/java/de/dennisguse/opentracks/ui/TrackListAdapter.java index c8034469c..bf4a99c81 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/TrackListAdapter.java +++ b/src/main/java/de/dennisguse/opentracks/ui/TrackListAdapter.java @@ -1,7 +1,6 @@ package de.dennisguse.opentracks.ui; import android.content.Intent; -import android.database.Cursor; import android.text.TextUtils; import android.util.SparseBooleanArray; import android.view.LayoutInflater; @@ -15,24 +14,20 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.ActionMode; import androidx.recyclerview.widget.RecyclerView; -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.TrackRecordedActivity; import de.dennisguse.opentracks.TrackRecordingActivity; -import de.dennisguse.opentracks.data.models.ActivityType; -import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.TrackListIterator; import de.dennisguse.opentracks.data.models.Track; -import de.dennisguse.opentracks.data.tables.TracksColumns; import de.dennisguse.opentracks.databinding.TrackListItemBinding; import de.dennisguse.opentracks.services.RecordingStatus; import de.dennisguse.opentracks.settings.UnitSystem; import de.dennisguse.opentracks.ui.util.ActivityUtils; import de.dennisguse.opentracks.ui.util.ListItemUtils; +import de.dennisguse.opentracks.util.AutoCloseableIterator; import de.dennisguse.opentracks.util.IntentUtils; public class TrackListAdapter extends RecyclerView.Adapter implements ActionMode.Callback { @@ -44,7 +39,7 @@ public class TrackListAdapter extends RecyclerView.Adapter cursor; private boolean selectionMode = false; private ActivityUtils.ContextualActionModeCallback actionModeCallback; private ActionMode actionMode; @@ -79,7 +74,7 @@ public class TrackListAdapter extends RecyclerView.Adapter cursor) { this.cursor = cursor; this.notifyDataSetChanged(); } @@ -138,12 +133,11 @@ public class TrackListAdapter extends RecyclerView.Adapter cursor) { + TrackListIterator.Item tracksListItem = cursor.get(); + trackId = tracksListItem.id(); - ActivityType activityType = ActivityType.findBy(cursor.getString(activityTypeIndex)); - String name = cursor.getString(nameIndex); - int markerCount = cursor.getInt(markerCountIndex); - Duration totalTime = Duration.ofMillis(cursor.getLong(totalTimeIndex)); - Distance totalDistance = Distance.of(cursor.getFloat(totalDistanceIndex)); - Instant startTime = Instant.ofEpochMilli(cursor.getLong(startTimeIndex)); - ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(cursor.getInt(startTimeOffsetIndex)); - String activityTypeLocalized = cursor.getString(activityTypeLocalizedIndex); - String description = cursor.getString(descriptionIndex); - trackId = new Track.Id(cursor.getLong(idIndex)); - - int iconId = activityType.getIconDrawableId(); + int iconId = tracksListItem.activityType().getIconDrawableId(); int iconDesc = R.string.image_track; boolean isRecordingThisTrackRecording = trackId.equals(recordingStatus.trackId()); @@ -217,26 +192,26 @@ public class TrackListAdapter extends RecyclerView.Adapter 0 ? View.VISIBLE : View.GONE); - viewBinding.trackListItemMarkerCount.setText(markerCount > 0 ? Integer.toString(markerCount) : null); + viewBinding.trackListItemMarkerCountIcon.setVisibility(tracksListItem.markerCount() > 0 ? View.VISIBLE : View.GONE); + viewBinding.trackListItemMarkerCount.setText(tracksListItem.markerCount() > 0 ? Integer.toString(tracksListItem.markerCount()) : null); if (!recordingStatus.isRecording()) { - ListItemUtils.setDateAndTime(context, viewBinding.trackListItemDate, viewBinding.trackListItemTime, startTime, zoneOffset); + ListItemUtils.setDateAndTime(context, viewBinding.trackListItemDate, viewBinding.trackListItemTime, tracksListItem.startTime(), tracksListItem.zoneOffset()); } else { viewBinding.trackListItemDate.setText(null); viewBinding.trackListItemTime.setText(null); } - viewBinding.trackListItemActivityTypeLocalized.setText(activityTypeLocalized); - viewBinding.trackListItemActivityTypeLocalized.setVisibility(TextUtils.isEmpty(activityTypeLocalized) ? View.GONE : View.VISIBLE); + viewBinding.trackListItemActivityTypeLocalized.setText(tracksListItem.activityTypeLocalized()); + viewBinding.trackListItemActivityTypeLocalized.setVisibility(TextUtils.isEmpty(tracksListItem.activityTypeLocalized()) ? View.GONE : View.VISIBLE); - viewBinding.trackListItemDescription.setText(description); - viewBinding.trackListItemDescription.setVisibility(TextUtils.isEmpty(description) ? View.GONE : View.VISIBLE); + viewBinding.trackListItemDescription.setText(tracksListItem.description()); + viewBinding.trackListItemDescription.setVisibility(TextUtils.isEmpty(tracksListItem.description()) ? View.GONE : View.VISIBLE); setSelected(selection.get((int) trackId.id())); } diff --git a/src/main/java/de/dennisguse/opentracks/util/AutoCloseableIterator.java b/src/main/java/de/dennisguse/opentracks/util/AutoCloseableIterator.java index 1abc11ab9..a9d98fae3 100644 --- a/src/main/java/de/dennisguse/opentracks/util/AutoCloseableIterator.java +++ b/src/main/java/de/dennisguse/opentracks/util/AutoCloseableIterator.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import java.util.Iterator; +//TODO All children use a cursor. Move this shared functionality here. public abstract class AutoCloseableIterator implements Iterator, AutoCloseable { /**