From 853469cb287a6de6bcbd84185b72f37cae58528e Mon Sep 17 00:00:00 2001 From: Leon Marz Date: Sun, 3 Sep 2023 13:02:50 +0200 Subject: [PATCH 1/4] ContentProviderUtils.java: Add searchTracks method --- .../opentracks/data/ContentProviderUtils.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java index 4c2282502..4d1d9a76c 100644 --- a/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java @@ -228,6 +228,31 @@ public class ContentProviderUtils { return tracks; } + public Cursor searchTracks(String searchQuery) { + // Needed, because MARKER_COUNT is a virtual column and has to be explicitly requested. + final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.UUID, TracksColumns.NAME, + TracksColumns.DESCRIPTION, TracksColumns.ACTIVITY_TYPE_LOCALIZED, TracksColumns.STARTTIME, + TracksColumns.STARTTIME_OFFSET, TracksColumns.STOPTIME, TracksColumns.MARKER_COUNT, + TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.MOVINGTIME, + TracksColumns.AVGSPEED, TracksColumns.AVGMOVINGSPEED, TracksColumns.MAXSPEED, + TracksColumns.MIN_ALTITUDE, TracksColumns.MAX_ALTITUDE, TracksColumns.ALTITUDE_GAIN, + TracksColumns.ALTITUDE_LOSS, TracksColumns.ICON + }; + + String selection = null; + String[] selectionArgs = null; + final String sortOrder = TracksColumns.STARTTIME + " DESC"; + + if (searchQuery != null) { + selection = TracksColumns.NAME + " LIKE ? OR " + + TracksColumns.DESCRIPTION + " LIKE ? OR " + + TracksColumns.ACTIVITY_TYPE_LOCALIZED + " LIKE ?"; + selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"}; + } + + return contentResolver.query(TracksColumns.CONTENT_URI, PROJECTION, selection, selectionArgs, sortOrder); + } + public Track getTrack(@NonNull Track.Id trackId) { try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())}, null)) { if (cursor != null && cursor.moveToNext()) { From 30c76de136498d40adde26a1cd7d81ae128bf433 Mon Sep 17 00:00:00 2001 From: Leon Marz Date: Sun, 3 Sep 2023 13:05:08 +0200 Subject: [PATCH 2/4] Add recycler view adapter for track list --- .../opentracks/ui/TrackListAdapter.java | 293 ++++++++++++++++++ .../opentracks/ui/util/ListItemUtils.java | 124 ++------ src/main/res/layout/list_item.xml | 146 --------- src/main/res/layout/track_list_item.xml | 117 +++++++ 4 files changed, 437 insertions(+), 243 deletions(-) create mode 100644 src/main/java/de/dennisguse/opentracks/ui/TrackListAdapter.java delete mode 100644 src/main/res/layout/list_item.xml create mode 100644 src/main/res/layout/track_list_item.xml diff --git a/src/main/java/de/dennisguse/opentracks/ui/TrackListAdapter.java b/src/main/java/de/dennisguse/opentracks/ui/TrackListAdapter.java new file mode 100644 index 000000000..e1b2b3a0c --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/ui/TrackListAdapter.java @@ -0,0 +1,293 @@ +package de.dennisguse.opentracks.ui; + +import android.app.ActivityOptions; +import android.content.Intent; +import android.database.Cursor; +import android.util.Pair; +import android.util.SparseBooleanArray; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +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.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.IntentUtils; +import de.dennisguse.opentracks.util.StringUtils; + +public class TrackListAdapter extends RecyclerView.Adapter implements ActionMode.Callback { + + private static final String TAG = TrackListAdapter.class.getSimpleName(); + + private final AppCompatActivity context; + private final RecyclerView recyclerView; + private final SparseBooleanArray selection = new SparseBooleanArray(); + private RecordingStatus recordingStatus; + private UnitSystem unitSystem; + private Cursor tracks; + private boolean selectionMode = false; + private ActivityUtils.ContextualActionModeCallback actionModeCallback; + private ActionMode actionMode; + + public TrackListAdapter(AppCompatActivity context, RecyclerView recyclerView, RecordingStatus recordingStatus, UnitSystem unitSystem) { + this.context = context; + this.recyclerView = recyclerView; + this.recordingStatus = recordingStatus; + this.unitSystem = unitSystem; + } + + public void setActionModeCallback(ActivityUtils.ContextualActionModeCallback actionModeCallback) { + this.actionModeCallback = actionModeCallback; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.track_list_item, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + ViewHolder viewHolder = (ViewHolder) holder; + + tracks.moveToPosition(position); + viewHolder.bind(tracks); + } + + @Override + public int getItemCount() { + if (tracks == null) { + return 0; + } + return tracks.getCount(); + } + + public void swapData(Cursor tracks) { + this.tracks = tracks; + this.notifyDataSetChanged(); + } + + public void updateRecordingStatus(RecordingStatus recordingStatus) { + this.recordingStatus = recordingStatus; + } + + public void updateUnitSystem(UnitSystem unitSystem) { + this.unitSystem = unitSystem; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mode.getMenuInflater().inflate(R.menu.list_context_menu, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + selectionMode = true; + actionModeCallback.onPrepare(menu, getCheckedPositions(), getCheckedIds(), true); + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (actionModeCallback.onClick(item.getItemId(), getCheckedPositions(), getCheckedIds())) { + mode.finish(); + } + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + selectionMode = false; + + setAllSelected(false); + selection.clear(); + + actionModeCallback.onDestroy(); + } + + public void setAllSelected(boolean isSelected) { + for (int i = 0; i < getItemCount(); i++) { + ViewHolder holder = (ViewHolder) recyclerView.findViewHolderForLayoutPosition(i); + holder.setSelected(isSelected); + } + } + + private int[] getCheckedPositions() { + List positions = new ArrayList<>(); + + for (int i = 0; i < selection.size(); i++) { + if (selection.valueAt(i)) { + positions.add(selection.keyAt(i)); + } + } + + return positions.stream().mapToInt(i -> i).toArray(); + } + + private long[] getCheckedIds() { + List ids = new ArrayList<>(); + + for (int i = 0; i < selection.size(); i++) { + if (selection.valueAt(i)) { + ViewHolder holder = (ViewHolder) recyclerView.findViewHolderForLayoutPosition(selection.keyAt(i)); + ids.add(holder.getId()); + } + } + + return ids.stream().mapToLong(i -> i).toArray(); + } + + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { + + private final TrackListItemBinding viewBinding; + private final View view; + + private ActivityType activityType; + private String name; + private int markerCount; + private Duration totalTime; + private Distance totalDistance; + private Instant startTime; + private ZoneOffset zoneOffset; + private String activityTypeLocalized; + private String description; + private Track.Id trackId; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + + viewBinding = TrackListItemBinding.bind(itemView); + view = itemView; + + view.setOnClickListener(this); + view.setOnLongClickListener(this); + } + + private void getData(Cursor track) { + final int idIndex = track.getColumnIndexOrThrow(TracksColumns._ID); + final int nameIndex = track.getColumnIndexOrThrow(TracksColumns.NAME); + final int descriptionIndex = track.getColumnIndexOrThrow(TracksColumns.DESCRIPTION); + final int activityTypeLocalizedIndex = track.getColumnIndexOrThrow(TracksColumns.ACTIVITY_TYPE_LOCALIZED); + final int startTimeIndex = track.getColumnIndexOrThrow(TracksColumns.STARTTIME); + final int startTimeOffsetIndex = track.getColumnIndexOrThrow(TracksColumns.STARTTIME_OFFSET); + final int totalDistanceIndex = track.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE); + final int totalTimeIndex = track.getColumnIndexOrThrow(TracksColumns.TOTALTIME); + final int iconIndex = track.getColumnIndexOrThrow(TracksColumns.ICON); + final int markerCountIndex = track.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT); + + activityType = ActivityType.findBy(track.getString(iconIndex)); + name = track.getString(nameIndex); + markerCount = track.getInt(markerCountIndex); + totalTime = Duration.ofMillis(track.getLong(totalTimeIndex)); + totalDistance = Distance.of(track.getFloat(totalDistanceIndex)); + startTime = Instant.ofEpochMilli(track.getLong(startTimeIndex)); + zoneOffset = ZoneOffset.ofTotalSeconds(track.getInt(startTimeOffsetIndex)); + activityTypeLocalized = track.getString(activityTypeLocalizedIndex); + description = track.getString(descriptionIndex); + trackId = new Track.Id(track.getLong(idIndex)); + } + + public void bind(Cursor track) { + + getData(track); + + int iconId = activityType.getIconDrawableId(); + int iconDesc = R.string.image_track; + + if (recordingStatus.isRecording()) { + iconId = R.drawable.ic_track_recording; + iconDesc = R.string.image_record; + } + + viewBinding.trackListItemIcon.setImageResource(iconId); + viewBinding.trackListItemIcon.setContentDescription(context.getString(iconDesc)); + + viewBinding.trackListItemName.setText(name); + + String timeDistanceText = ListItemUtils.getTimeDistanceText(context, unitSystem, recordingStatus.isRecording(), totalTime, totalDistance, markerCount); + viewBinding.trackListItemTimeDistance.setText(timeDistanceText); + + viewBinding.trackListItemMarkerCountIcon.setVisibility(markerCount > 0 ? View.VISIBLE : View.GONE); + viewBinding.trackListItemMarkerCount.setText(markerCount > 0 ? Integer.toString(markerCount) : null); + + if (!recordingStatus.isRecording()) { + ListItemUtils.setDateAndTime(context, viewBinding.trackListItemDate, viewBinding.trackListItemTime, startTime, zoneOffset); + } else { + viewBinding.trackListItemDate.setText(null); + viewBinding.trackListItemTime.setText(null); + } + + String category = activityType == null ? activityTypeLocalized : null; + String categoryDescription = StringUtils.getCategoryDescription(category, description); + viewBinding.trackListItemCategoryDescription.setText(categoryDescription); + viewBinding.trackListItemCategoryDescription.setVisibility(categoryDescription.equals("") ? View.GONE : View.VISIBLE); + + setSelected(selection.get(getLayoutPosition())); + } + + public void setSelected(boolean isSelected) { + selection.put(getLayoutPosition(), isSelected); + view.setActivated(isSelected); + } + + public long getId() { + return trackId.id(); + } + + + @Override + public void onClick(View v) { + if (selectionMode) { + setSelected(!view.isActivated()); + actionMode.invalidate(); + return; + } + + if (recordingStatus.isRecording() && trackId.equals(recordingStatus.getTrackId())) { + // Is recording -> open record activity. + Intent newIntent = IntentUtils.newIntent(context, TrackRecordingActivity.class) + .putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId); + context.startActivity(newIntent); + } else { + // Not recording -> open detail activity. + Intent newIntent = IntentUtils.newIntent(context, TrackRecordedActivity.class) + .putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId); + ActivityOptions activityOptions = ActivityOptions.makeSceneTransitionAnimation( + context, + new Pair<>(view.findViewById(R.id.track_list_item_icon), TrackRecordedActivity.VIEW_TRACK_ICON)); + context.startActivity(newIntent, activityOptions.toBundle()); + } + } + + @Override + public boolean onLongClick(View v) { + actionMode = context.startSupportActionMode(TrackListAdapter.this); + setSelected(true); + return true; + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/ui/util/ListItemUtils.java b/src/main/java/de/dennisguse/opentracks/ui/util/ListItemUtils.java index b07b7455d..71a82701b 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/util/ListItemUtils.java +++ b/src/main/java/de/dennisguse/opentracks/ui/util/ListItemUtils.java @@ -18,18 +18,20 @@ package de.dennisguse.opentracks.ui.util; import android.content.Context; import android.view.View; -import android.view.ViewGroup.LayoutParams; -import android.widget.ImageView; import android.widget.TextView; import androidx.core.content.ContextCompat; +import java.time.Duration; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.DistanceFormatter; +import de.dennisguse.opentracks.settings.UnitSystem; import de.dennisguse.opentracks.util.StringUtils; /** @@ -42,101 +44,6 @@ public class ListItemUtils { private ListItemUtils() { } - /** - * Sets a list item. - * - * @param context the context - * @param view the list item view - * @param isRecording true if recording - * @param iconId the icon id - * @param iconContentDescriptionId the icon content description id - * @param name the name value - * @param totalTime the total time value - * @param totalDistance the total distance value - * @param markerCount the marker count - * @param offsetDateTime the start time with offset - * @param category the category value - * @param description the description value - * @param hasPhoto true if this list item has photo - */ - public static void setListItem(Context context, View view, boolean isRecording, int iconId, int iconContentDescriptionId, String name, String totalTime, String totalDistance, int markerCount, OffsetDateTime offsetDateTime, String category, String description, boolean hasPhoto) { - // Set icon - if (isRecording) { - iconId = R.drawable.ic_track_recording; - iconContentDescriptionId = R.string.image_record; - } - - ImageView iconImageView = view.findViewById(R.id.list_item_icon); - iconImageView.setImageResource(iconId); - iconImageView.setContentDescription(context.getString(iconContentDescriptionId)); - - // Set name - TextView nameTextView = view.findViewById(R.id.list_item_name); - setTextView(context, nameTextView, name, hasPhoto); - - // Set totalTime/totalDistance - TextView timeDistanceTextView = view.findViewById(R.id.list_item_time_distance); - String timeDistanceText; - if (isRecording) { - timeDistanceText = context.getString(R.string.generic_recording); - } else { - // Match list_item_time_distance in list_item.xml - timeDistanceText = getTimeDistance(totalTime, totalDistance); - if (markerCount > 0) { - timeDistanceText += " ‧"; - } - } - setTextView(context, timeDistanceTextView, timeDistanceText, hasPhoto); - - // Set markerCount - ImageView markerCountIcon = view.findViewById(R.id.list_item_marker_count_icon); - TextView markerCountTextView = view.findViewById(R.id.list_item_marker_count); - boolean hasMarker = markerCount > 0; - markerCountIcon.setVisibility(hasMarker ? View.VISIBLE : View.GONE); - String markerCountValue = hasMarker ? String.valueOf(markerCount) : null; - if (hasMarker) { - // Scale markerCountIcon - int lineHeight = markerCountTextView.getLineHeight(); - LayoutParams layoutParams = markerCountIcon.getLayoutParams(); - layoutParams.width = lineHeight; - layoutParams.height = lineHeight; - } - setTextView(context, markerCountTextView, markerCountValue, hasPhoto); - - // Set date/time - TextView dateTextView = view.findViewById(R.id.list_item_date); - TextView timeTextView = view.findViewById(R.id.list_item_time); - String dateValue = null; - String timeValue = null; - if (!isRecording) { - dateValue = StringUtils.formatDateTodayRelative(context, offsetDateTime); - String pattern = "HH:mm"; - if (!offsetDateTime.getOffset().equals(OffsetDateTime.now().getOffset())) { - pattern = "HH:mm x"; - } - timeValue = offsetDateTime.format(DateTimeFormatter.ofPattern(pattern)); - } - setTextView(context, dateTextView, dateValue, hasPhoto); - setTextView(context, timeTextView, timeValue, hasPhoto); - - // Set category and description - TextView categoryDescriptionTextView = view.findViewById(R.id.list_item_category_description); - String categoryDescription = isRecording ? null : StringUtils.getCategoryDescription(category, description); - - // Place categoryDescription in either ownerTimeDistanceTextView or categoryDescriptionTextView - if (timeDistanceTextView.getVisibility() == View.GONE && markerCountIcon.getVisibility() == View.GONE) { - setTextView(context, categoryDescriptionTextView, null, hasPhoto); - // Match list_item_category_description in list_item.xml - timeDistanceTextView.setSingleLine(false); - timeDistanceTextView.setMaxLines(2); - setTextView(context, timeDistanceTextView, categoryDescription, hasPhoto); - } else { - // Match list_item_time_distance in list_item.xml - timeDistanceTextView.setSingleLine(true); - setTextView(context, categoryDescriptionTextView, categoryDescription, hasPhoto); - } - } - /** * Gets a string for share owner, total time, and total distance. * @@ -160,6 +67,29 @@ public class ListItemUtils { return builder.toString(); } + public static String getTimeDistanceText(Context context, UnitSystem unitSystem, boolean isRecording, Duration totalTime, Distance totalDistance, int markerCount) { + String timeDistanceText; + if (isRecording) { + timeDistanceText = context.getString(R.string.generic_recording); + } else { + // Match list_item_time_distance in list_item.xml + + String time = StringUtils.formatElapsedTime(totalTime); + // TODO unit system + String distance = DistanceFormatter.Builder() + .setUnit(unitSystem) + .build(context) + .formatDistance(totalDistance); + + timeDistanceText = getTimeDistance(time, distance); + if (markerCount > 0) { + timeDistanceText += " ‧"; + } + } + + return timeDistanceText; + } + /** * Sets a text view. * diff --git a/src/main/res/layout/list_item.xml b/src/main/res/layout/list_item.xml deleted file mode 100644 index 6273c422c..000000000 --- a/src/main/res/layout/list_item.xml +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/res/layout/track_list_item.xml b/src/main/res/layout/track_list_item.xml new file mode 100644 index 000000000..3e61e1ba0 --- /dev/null +++ b/src/main/res/layout/track_list_item.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 7776b51f05327df7603f3978adeb789939421913 Mon Sep 17 00:00:00 2001 From: Leon Marz Date: Sun, 3 Sep 2023 17:41:02 +0200 Subject: [PATCH 3/4] TrackListActivity: Set launchMode to singleTop --- src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 391e6c674..353248b85 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -333,7 +333,8 @@ limitations under the License. + android:exported="true" + android:launchMode="singleTop"> From e18017d500cf2b1d2c6d9f12fe5a3ffd9415e7d8 Mon Sep 17 00:00:00 2001 From: Leon Marz Date: Sun, 3 Sep 2023 17:42:19 +0200 Subject: [PATCH 4/4] TrackListActivity: Switch to RecyclerView Part of: #1613 --- .../opentracks/TrackListActivity.java | 193 ++++-------------- src/main/res/layout/track_list.xml | 2 +- 2 files changed, 43 insertions(+), 152 deletions(-) diff --git a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java index bf10ba549..a8fdbc933 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java @@ -16,7 +16,6 @@ package de.dennisguse.opentracks; -import android.app.ActivityOptions; import android.app.SearchManager; import android.content.Context; import android.content.Intent; @@ -26,37 +25,27 @@ import android.graphics.drawable.AnimatedVectorDrawable; import android.location.LocationManager; import android.os.Bundle; import android.provider.Settings; -import android.util.Pair; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Toast; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.SearchView; import androidx.core.content.ContextCompat; -import androidx.cursoradapter.widget.ResourceCursorAdapter; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; +import androidx.recyclerview.widget.LinearLayoutManager; import com.google.android.material.button.MaterialButton; -import java.time.Duration; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Objects; -import de.dennisguse.opentracks.data.models.ActivityType; -import de.dennisguse.opentracks.data.models.Distance; -import de.dennisguse.opentracks.data.models.DistanceFormatter; +import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.Track; -import de.dennisguse.opentracks.data.tables.TracksColumns; import de.dennisguse.opentracks.databinding.TrackListBinding; import de.dennisguse.opentracks.services.RecordingStatus; import de.dennisguse.opentracks.services.TrackRecordingService; @@ -66,15 +55,14 @@ import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.UnitSystem; import de.dennisguse.opentracks.share.ShareUtils; +import de.dennisguse.opentracks.ui.TrackListAdapter; import de.dennisguse.opentracks.ui.aggregatedStatistics.AggregatedStatisticsActivity; import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.ui.markers.MarkerListActivity; import de.dennisguse.opentracks.ui.util.ActivityUtils; -import de.dennisguse.opentracks.ui.util.ListItemUtils; import de.dennisguse.opentracks.util.IntentDashboardUtils; import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.PermissionRequester; -import de.dennisguse.opentracks.util.StringUtils; /** * An activity displaying a list of tracks. @@ -87,12 +75,10 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co // The following are set in onCreate private TrackRecordingServiceConnection trackRecordingServiceConnection; - private ResourceCursorAdapter resourceCursorAdapter; + private TrackListAdapter adapter; private TrackListBinding viewBinding; - private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack(); - // Preferences private UnitSystem unitSystem = UnitSystem.defaultUnitSystem(); @@ -128,11 +114,14 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { if (PreferencesUtils.isKey(R.string.stats_units_key, key)) { unitSystem = PreferencesUtils.getUnitSystem(); + if (adapter != null) { + adapter.updateUnitSystem(unitSystem); + } } if (key != null) { runOnUiThread(() -> { TrackListActivity.this.invalidateOptionsMenu(); - loaderCallbacks.restart(); + loadData(); }); } }; @@ -140,6 +129,8 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co // Menu items private MenuItem searchMenuItem; + private String searchQuery; + private final TrackRecordingServiceConnection.Callback bindChangedCallback = (service, unused) -> { service.getRecordingStatusObservable() .observe(TrackListActivity.this, this::onRecordingStatusChanged); @@ -180,63 +171,10 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co } }); - viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView); - viewBinding.trackList.setOnItemClickListener((parent, view, position, trackIdId) -> { - Track.Id trackId = new Track.Id(trackIdId); - if (recordingStatus.isRecording() && trackId.equals(recordingStatus.getTrackId())) { - // Is recording -> open record activity. - Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class) - .putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId); - startActivity(newIntent); - } else { - // Not recording -> open detail activity. - Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordedActivity.class) - .putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId); - ActivityOptions activityOptions = ActivityOptions.makeSceneTransitionAnimation( - this, - new Pair<>(view.findViewById(R.id.list_item_icon), TrackRecordedActivity.VIEW_TRACK_ICON)); - startActivity(newIntent, activityOptions.toBundle()); - } - }); - - resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) { - @Override - public void bindView(View view, Context context, Cursor cursor) { - int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID); - int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON); - int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME); - int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME); - int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE); - int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME); - int startTimeOffsetIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME_OFFSET); - int activityTypeIndex = cursor.getColumnIndexOrThrow(TracksColumns.ACTIVITY_TYPE_LOCALIZED); - int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION); - int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT); - - Track.Id trackId = new Track.Id(cursor.getLong(idIndex)); - boolean isRecording = trackId.equals(recordingStatus.getTrackId()); - String icon = cursor.getString(iconIndex); - int iconId = ActivityType.findBy(icon) - .getIconDrawableId(); - String name = cursor.getString(nameIndex); - String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex))); - String totalDistance = DistanceFormatter.Builder() - .setUnit(unitSystem) - .build(TrackListActivity.this).formatDistance(Distance.of(cursor.getDouble(totalDistanceIndex))); - int markerCount = cursor.getInt(markerCountIndex); - long startTime = cursor.getLong(startTimeIndex); - int startTimeOffset = cursor.getInt(startTimeOffsetIndex); - String activityType = icon != null && !icon.equals("") ? null : cursor.getString(activityTypeIndex); - String description = cursor.getString(descriptionIndex); - - ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, - iconId, R.string.image_track, name, totalTime, totalDistance, markerCount, - OffsetDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneOffset.ofTotalSeconds(startTimeOffset)), - activityType, description, false); - } - }; - viewBinding.trackList.setAdapter(resourceCursorAdapter); - ActivityUtils.configureListViewContextualMenu(viewBinding.trackList, contextualActionModeCallback); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + adapter = new TrackListAdapter(this, viewBinding.trackList, recordingStatus, unitSystem); + viewBinding.trackList.setLayoutManager(layoutManager); + viewBinding.trackList.setAdapter(adapter); viewBinding.trackListFabAction.setOnClickListener((view) -> { if (recordingStatus.isRecording()) { @@ -271,8 +209,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co }); setSupportActionBar(viewBinding.trackListToolbar); - - loadData(getIntent()); + adapter.setActionModeCallback(contextualActionModeCallback); } private void requestRequiredPermissions() { @@ -293,7 +230,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co // Update UI this.invalidateOptionsMenu(); - LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks); + loadData(); // Float button setFloatButton(); @@ -312,6 +249,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co super.onDestroy(); viewBinding = null; trackRecordingServiceConnection = null; + adapter = null; } @Override @@ -333,10 +271,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co @Override public boolean onPrepareOptionsMenu(Menu menu) { updateGpsMenuItem(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording()); - - SearchView searchView = (SearchView) searchMenuItem.getActionView(); - searchView.setQuery("", false); - return super.onPrepareOptionsMenu(menu); } @@ -375,11 +309,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co return super.onKeyUp(keyCode, event); } - @Override - public void overridePendingTransition(int enterAnim, int exitAnim) { - //Disable animations as it is weird going into searchMode; looks okay for SplashScreen. - } - @Override public void onBackPressed() { SearchView searchView = (SearchView) searchMenuItem.getActionView(); @@ -387,8 +316,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co searchView.setIconified(true); } - if (loaderCallbacks.getSearchQuery() != null) { - loaderCallbacks.setSearch(null); + if (searchQuery != null) { + searchQuery = null; + loadData(); return; } @@ -396,19 +326,30 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co } @Override - public void onNewIntent(Intent intent) { + protected void onNewIntent(Intent intent) { super.onNewIntent(intent); - setIntent(intent); - loadData(intent); - } - private void loadData(Intent intent) { - String searchQuery = null; if (Intent.ACTION_SEARCH.equals(intent.getAction())) { searchQuery = intent.getStringExtra(SearchManager.QUERY); + } else { + searchQuery = null; } + } - loaderCallbacks.setSearch(searchQuery); + private void loadData() { + viewBinding.trackListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.app_name))); + + Cursor tracks = new ContentProviderUtils(this).searchTracks(searchQuery); + + adapter.swapData(tracks); + + if (tracks.getCount() == 0) { + viewBinding.trackListEmptyView.setVisibility(View.VISIBLE); + viewBinding.trackList.setVisibility(View.GONE); + } else { + viewBinding.trackListEmptyView.setVisibility(View.GONE); + viewBinding.trackList.setVisibility(View.VISIBLE); + } } @Override @@ -490,64 +431,13 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co } if (itemId == R.id.list_context_menu_select_all) { - for (int i = 0; i < viewBinding.trackList.getCount(); i++) { - viewBinding.trackList.setItemChecked(i, true); - } + adapter.setAllSelected(true); return false; } return false; } - private class TrackLoaderCallBack implements LoaderManager.LoaderCallbacks { - - private String searchQuery = null; - - public String getSearchQuery() { - return searchQuery; - } - - public void setSearch(String searchQuery) { - this.searchQuery = searchQuery; - restart(); - viewBinding.trackListToolbar.setTitle(searchQuery == null ? getString(R.string.app_name) : searchQuery); - } - - public void restart() { - LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks); - } - - @NonNull - @Override - public Loader onCreateLoader(int arg0, Bundle arg1) { - final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME, - TracksColumns.DESCRIPTION, TracksColumns.ACTIVITY_TYPE_LOCALIZED, TracksColumns.STARTTIME, TracksColumns.STARTTIME_OFFSET, - TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, TracksColumns.MARKER_COUNT}; - - final String sortOrder = TracksColumns.STARTTIME + " DESC"; - - if (searchQuery == null) { - return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null, null, sortOrder); - } else { - final String SEARCH_QUERY = TracksColumns.NAME + " LIKE ? OR " + - TracksColumns.DESCRIPTION + " LIKE ? OR " + - TracksColumns.ACTIVITY_TYPE_LOCALIZED + " LIKE ?"; - final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"}; - return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, sortOrder); - } - } - - @Override - public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { - resourceCursorAdapter.swapCursor(cursor); - } - - @Override - public void onLoaderReset(@NonNull Loader loader) { - resourceCursorAdapter.swapCursor(null); - } - } - public void onGpsStatusChanged(GpsStatusValue newStatus) { gpsStatusValue = newStatus; updateGpsMenuItem(true, recordingStatus.isRecording()); @@ -561,5 +451,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co private void onRecordingStatusChanged(RecordingStatus status) { recordingStatus = status; setFloatButton(); + adapter.updateRecordingStatus(recordingStatus); } } diff --git a/src/main/res/layout/track_list.xml b/src/main/res/layout/track_list.xml index 91e315454..53e873f26 100644 --- a/src/main/res/layout/track_list.xml +++ b/src/main/res/layout/track_list.xml @@ -39,7 +39,7 @@ limitations under the License. android:paddingBottom="?attr/actionBarSize" app:layout_behavior="@string/appbar_scrolling_view_behavior"> -