From 7afa4b86d40aadf1dab375e62f4c41eb772345c8 Mon Sep 17 00:00:00 2001 From: Leon Marz Date: Sun, 20 Aug 2023 12:46:21 +0200 Subject: [PATCH 1/3] ContentProviderUtils: Add searchMarkers method --- .../opentracks/data/ContentProviderUtils.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java index 4b95f6096..8d606e52d 100644 --- a/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java @@ -548,6 +548,35 @@ public class ContentProviderUtils { return contentResolver.query(MarkerColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder); } + public List searchMarkers(Track.Id trackId, String query) { + String selection = null; + String[] selectionArgs = null; + String sortOrder = null; + + if (query == null) { + if (trackId != null) { + selection = MarkerColumns.TRACKID + " = ?"; + selectionArgs = new String[]{Long.toString(trackId.id())}; + } + } else { + selection = MarkerColumns.NAME + " LIKE ? OR " + + MarkerColumns.DESCRIPTION + " LIKE ? OR " + + MarkerColumns.CATEGORY + " LIKE ?"; + selectionArgs = new String[]{"%" + query + "%", "%" + query + "%", "%" + query + "%"}; + sortOrder = MarkerColumns.DEFAULT_SORT_ORDER + " DESC"; + } + + ArrayList markers = new ArrayList<>(); + try (Cursor cursor = getMarkerCursor(null, selection, selectionArgs, sortOrder, -1)) { + if (cursor.moveToFirst()) { + do { + markers.add(createMarker(cursor)); + } while (cursor.moveToNext()); + } + } + return markers; + } + /** * Fills a {@link TrackPoint} from a cursor. * From 5978886a1838259a5179a26dd51256c79dc16d20 Mon Sep 17 00:00:00 2001 From: Leon Marz Date: Sun, 20 Aug 2023 12:47:30 +0200 Subject: [PATCH 2/3] Add recycler view adapter for marker list --- .../ui/markers/MarkerListAdapter.java | 276 ++++++++++++++++++ .../opentracks/ui/util/ListItemUtils.java | 15 + .../opentracks/ui/util/ThemeUtils.java | 11 + src/main/res/layout/marker_list_item.xml | 96 ++++++ 4 files changed, 398 insertions(+) create mode 100644 src/main/java/de/dennisguse/opentracks/ui/markers/MarkerListAdapter.java create mode 100644 src/main/res/layout/marker_list_item.xml diff --git a/src/main/java/de/dennisguse/opentracks/ui/markers/MarkerListAdapter.java b/src/main/java/de/dennisguse/opentracks/ui/markers/MarkerListAdapter.java new file mode 100644 index 000000000..f02a65126 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/ui/markers/MarkerListAdapter.java @@ -0,0 +1,276 @@ +package de.dennisguse.opentracks.ui.markers; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.ThumbnailUtils; +import android.net.Uri; +import android.util.Log; +import android.util.LruCache; +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 android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.view.ActionMode; +import androidx.recyclerview.widget.RecyclerView; + +import java.io.IOException; +import java.io.InputStream; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.data.ContentProviderUtils; +import de.dennisguse.opentracks.data.models.Marker; +import de.dennisguse.opentracks.databinding.MarkerListItemBinding; +import de.dennisguse.opentracks.ui.util.ActivityUtils; +import de.dennisguse.opentracks.ui.util.ExecutorListViewService; +import de.dennisguse.opentracks.ui.util.ListItemUtils; +import de.dennisguse.opentracks.ui.util.ThemeUtils; +import de.dennisguse.opentracks.util.IntentUtils; +import de.dennisguse.opentracks.util.StringUtils; + +public class MarkerListAdapter extends RecyclerView.Adapter implements ActionMode.Callback { + + private static final String TAG = MarkerListAdapter.class.getSimpleName(); + + private final AppCompatActivity context; + private final RecyclerView recyclerView; + private List markers; + private final SparseBooleanArray selection; + private boolean selectionMode; + private ActivityUtils.ContextualActionModeCallback actionModeCallback; + private ActionMode actionMode; + + private final ExecutorListViewService executorService = new ExecutorListViewService<>(128); + + private final LruCache memoryCache; + + public MarkerListAdapter(AppCompatActivity context, RecyclerView recyclerView, List markers) { + this.context = context; + this.recyclerView = recyclerView; + this.markers = markers; + + selection = new SparseBooleanArray(); + selectionMode = false; + + // Get max available VM memory, exceeding this amount will throw an OutOfMemory exception. + final long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final int cacheSize = (int) (Runtime.getRuntime().maxMemory() - usedMemory) / 8; + + memoryCache = new LruCache<>(cacheSize) { + @Override + protected int sizeOf(Marker.Id key, Bitmap value) { + return value.getByteCount(); + } + }; + } + + 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.marker_list_item, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + ViewHolder viewHolder = (ViewHolder) holder; + + viewHolder.bind(markers.get(position)); + } + + @Override + public int getItemCount() { + if (markers == null) { + return 0; + } + return markers.size(); + } + + public void swapData(List markers) { + this.markers = markers; + this.notifyDataSetChanged(); + } + + @Override + public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { + actionMode.getMenuInflater().inflate(R.menu.list_context_menu, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { + selectionMode = true; + actionModeCallback.onPrepare(menu, getCheckedPositions(), getCheckedIds(), true); + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { + if (actionModeCallback.onClick(menuItem.getItemId(), getCheckedPositions(), getCheckedIds())) { + actionMode.finish(); + } + return true; + } + + @Override + public void onDestroyActionMode(ActionMode actionMode) { + 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 MarkerListItemBinding viewBinding; + private final View view; + + private Marker marker; + + public ViewHolder(@NonNull View view) { + super(view); + + viewBinding = MarkerListItemBinding.bind(view); + this.view = view; + + view.setOnClickListener(this); + view.setOnLongClickListener(this); + } + + public void bind(Marker marker) { + this.marker = marker; + + viewBinding.markerListItemPhoto.setVisibility(marker.hasPhoto() ? View.VISIBLE : View.GONE); + viewBinding.markerListItemTextGradient.setVisibility(marker.hasPhoto() ? View.VISIBLE : View.GONE); + if (marker.hasPhoto()) { + int height = ThemeUtils.getPhotoHeight(context); + ViewGroup.LayoutParams params = viewBinding.markerListItemPhoto.getLayoutParams(); + params.height = height; + viewBinding.markerListItemPhoto.setLayoutParams(params); + viewBinding.markerListItemTextGradient.setLayoutParams(params); + + asyncLoadPhoto(viewBinding.markerListItemPhoto, marker.getPhotoURI(), marker.getId()); + } + + viewBinding.markerListItemName.setText(marker.getName()); + + { + ZoneOffset timeZone = new ContentProviderUtils(context).getTrack(marker.getTrackId()).getZoneOffset(); + ListItemUtils.setDateAndTime(context, viewBinding.markerListItemDate, viewBinding.markerListItemTime, marker.getTime(), timeZone); + } + + String categoryDescription = StringUtils.getCategoryDescription(marker.getCategory(), marker.getDescription()); + viewBinding.markerListItemTimeDistance.setText(categoryDescription); + + setSelected(selection.get(getLayoutPosition())); + } + + public void setSelected(boolean isSelected) { + selection.put(getLayoutPosition(), isSelected); + view.setActivated(isSelected); + } + + public long getId() { + return marker.getId().id(); + } + + @Override + public void onClick(View view) { + if (selectionMode) { + setSelected(!view.isActivated()); + actionMode.invalidate(); + } else { + Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class) + .putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, marker.getId()); + context.startActivity(intent); + } + } + + @Override + public boolean onLongClick(View view) { + actionMode = context.startSupportActionMode(MarkerListAdapter.this); + setSelected(true); + return true; + } + } + + /** + * It loads the photoUrl in the imageView from view. + * It takes the photo from cache or from storage if isn't in the cache. + * + * @param imageView view object where photo will be loaded. + * @param photoUri photo's url. + * @param id marker's id where photo belong. + */ + private void asyncLoadPhoto(ImageView imageView, Uri photoUri, Marker.Id id) { + + Bitmap bm = memoryCache.get(id); + imageView.setImageBitmap(bm); + if (bm != null) { + return; + } + + executorService.execute(id, () -> { + try (InputStream inputStream = context.getContentResolver().openInputStream(photoUri)) { + Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + int height = ThemeUtils.getPhotoHeight(context); + Log.d(TAG, "Width : " + (bitmap.getWidth() / (bitmap.getHeight() / height)) + " | Height: " + height); + final Bitmap finalPhoto = ThumbnailUtils.extractThumbnail(bitmap, bitmap.getWidth() / (bitmap.getHeight() / height), height); + context.runOnUiThread(() -> imageView.setImageBitmap(finalPhoto)); + memoryCache.put(id, finalPhoto); + } catch (IOException e) { + Log.e(TAG, "Failed to image " + e); + } + }); + } +} 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 88db83581..b07b7455d 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/util/ListItemUtils.java +++ b/src/main/java/de/dennisguse/opentracks/ui/util/ListItemUtils.java @@ -24,7 +24,9 @@ import android.widget.TextView; import androidx.core.content.ContextCompat; +import java.time.Instant; import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import de.dennisguse.opentracks.R; @@ -180,4 +182,17 @@ public class ListItemUtils { textView.setShadowLayer(0, 0, 0, 0); } } + + public static void setDateAndTime(Context context, TextView dateView, TextView timeView, Instant time, ZoneOffset timeZone) { + OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(time, timeZone); + String dateContent = StringUtils.formatDateTodayRelative(context, offsetDateTime); + String pattern = "HH:mm"; + if (!offsetDateTime.getOffset().equals(OffsetDateTime.now().getOffset())) { + pattern = "HH:mm x"; + } + String timeContent = offsetDateTime.format(DateTimeFormatter.ofPattern(pattern)); + + dateView.setText(dateContent); + timeView.setText(timeContent); + } } diff --git a/src/main/java/de/dennisguse/opentracks/ui/util/ThemeUtils.java b/src/main/java/de/dennisguse/opentracks/ui/util/ThemeUtils.java index bc3845899..8725c663f 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/util/ThemeUtils.java +++ b/src/main/java/de/dennisguse/opentracks/ui/util/ThemeUtils.java @@ -52,4 +52,15 @@ public class ThemeUtils { typedArray.recycle(); return fontSize; } + + /** + * Gets the photo height. + */ + public static int getPhotoHeight(Context context) { + int[] attrs = new int[]{android.R.attr.listPreferredItemHeight}; + TypedArray typeArray = context.obtainStyledAttributes(attrs); + int height = typeArray.getDimensionPixelSize(0, 128); + typeArray.recycle(); + return 2 * height; + } } diff --git a/src/main/res/layout/marker_list_item.xml b/src/main/res/layout/marker_list_item.xml new file mode 100644 index 000000000..39d301535 --- /dev/null +++ b/src/main/res/layout/marker_list_item.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From c256cbe07da4b577641b51697f78f9311a765237 Mon Sep 17 00:00:00 2001 From: Leon Marz Date: Sun, 20 Aug 2023 12:49:26 +0200 Subject: [PATCH 3/3] MarkerListActivity: Switch to RecyclerView Part of: #1613 --- .../ui/markers/MarkerListActivity.java | 156 ++++---------- .../markers/MarkerResourceCursorAdapter.java | 195 ------------------ src/main/res/layout/marker_list.xml | 2 +- 3 files changed, 42 insertions(+), 311 deletions(-) delete mode 100644 src/main/java/de/dennisguse/opentracks/ui/markers/MarkerResourceCursorAdapter.java diff --git a/src/main/java/de/dennisguse/opentracks/ui/markers/MarkerListActivity.java b/src/main/java/de/dennisguse/opentracks/ui/markers/MarkerListActivity.java index aa97b8779..228818c33 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/markers/MarkerListActivity.java +++ b/src/main/java/de/dennisguse/opentracks/ui/markers/MarkerListActivity.java @@ -18,19 +18,16 @@ package de.dennisguse.opentracks.ui.markers; import android.app.SearchManager; import android.content.Intent; -import android.database.Cursor; import android.os.Bundle; -import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; import androidx.appcompat.widget.SearchView; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; +import androidx.recyclerview.widget.LinearLayoutManager; +import java.util.List; import java.util.Objects; import de.dennisguse.opentracks.AbstractActivity; @@ -38,15 +35,12 @@ import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.Marker; import de.dennisguse.opentracks.data.models.Track; -import de.dennisguse.opentracks.data.tables.MarkerColumns; import de.dennisguse.opentracks.databinding.MarkerListBinding; import de.dennisguse.opentracks.services.RecordingStatus; import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.share.ShareUtils; -import de.dennisguse.opentracks.ui.markers.DeleteMarkerDialogFragment.DeleteMarkerCaller; import de.dennisguse.opentracks.ui.util.ActivityUtils; -import de.dennisguse.opentracks.ui.util.ScrollVisibleViews; import de.dennisguse.opentracks.util.IntentUtils; /** @@ -54,23 +48,20 @@ import de.dennisguse.opentracks.util.IntentUtils; * * @author Leif Hendrik Wilden */ -public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller { +public class MarkerListActivity extends AbstractActivity { public static final String EXTRA_TRACK_ID = "track_id"; - private static final String TAG = MarkerListActivity.class.getSimpleName(); - private ContentProviderUtils contentProviderUtils; private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT; - private Track track; - private MarkerResourceCursorAdapter resourceCursorAdapter; + private Track.Id trackId; + + private MarkerListAdapter adapter; private MarkerListBinding viewBinding; - private final MarkerLoaderCallback loaderCallbacks = new MarkerLoaderCallback(); - private TrackRecordingServiceConnection trackRecordingServiceConnection; private final TrackRecordingServiceConnection.Callback bindCallback = (service, unused) -> service.getRecordingStatusObservable() @@ -105,33 +96,24 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker private MenuItem insertMarkerMenuItem; private MenuItem searchMenuItem; + private String searchQuery; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); - Track.Id trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID); - contentProviderUtils = new ContentProviderUtils(this); - track = trackId != null ? contentProviderUtils.getTrack(trackId) : null; - - viewBinding.markerList.setEmptyView(viewBinding.markerListEmpty); - viewBinding.markerList.setOnItemClickListener((parent, view, position, id) -> { - resourceCursorAdapter.markerInvalid(id); - Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class) - .putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, new Marker.Id(id)); - startActivity(intent); - }); - - resourceCursorAdapter = new MarkerResourceCursorAdapter(this, R.layout.list_item); - ScrollVisibleViews scrollVisibleViews = new ScrollVisibleViews(resourceCursorAdapter); - viewBinding.markerList.setOnScrollListener(scrollVisibleViews); - viewBinding.markerList.setAdapter(resourceCursorAdapter); - ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback); + trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID); trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + adapter = new MarkerListAdapter(this, viewBinding.markerList, null); + viewBinding.markerList.setLayoutManager(layoutManager); + viewBinding.markerList.setAdapter(adapter); + setSupportActionBar(viewBinding.markerListToolbar); + adapter.setActionModeCallback(contextualActionModeCallback); } @Override @@ -145,7 +127,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker super.onResume(); trackRecordingServiceConnection.bind(this); this.invalidateOptionsMenu(); - loadData(getIntent()); + loadData(); } @Override @@ -157,9 +139,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker @Override protected void onDestroy() { super.onDestroy(); - resourceCursorAdapter.clear(); viewBinding = null; - resourceCursorAdapter = null; + adapter = null; contentProviderUtils = null; } @@ -183,15 +164,15 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker @Override public boolean onPrepareOptionsMenu(Menu menu) { - insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingStatus.getTrackId())); + insertMarkerMenuItem.setVisible(trackId != null && trackId.equals(recordingStatus.getTrackId())); return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (track != null && item.getItemId() == R.id.marker_list_insert_marker) { + if (trackId != null && item.getItemId() == R.id.marker_list_insert_marker) { Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class) - .putExtra(MarkerEditActivity.EXTRA_TRACK_ID, track.getId()); + .putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId); startActivity(intent); return true; } @@ -229,7 +210,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker if (itemId == R.id.list_context_menu_edit) { if (markerIds.length == 1) { - resourceCursorAdapter.markerInvalid(markerIds[0].id()); Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class) .putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]); startActivity(intent); @@ -243,23 +223,13 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker } if (itemId == R.id.list_context_menu_select_all) { - for (int i = 0; i < viewBinding.markerList.getCount(); i++) { - viewBinding.markerList.setItemChecked(i, true); - } + adapter.setAllSelected(true); return false; } return false; } - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) { - return true; - } - return super.onKeyUp(keyCode, event); - } - @Override public void onBackPressed() { SearchView searchView = (SearchView) searchMenuItem.getActionView(); @@ -267,8 +237,9 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker searchView.setIconified(true); } - if (loaderCallbacks.getSearchQuery() != null) { - loaderCallbacks.setSearch(null); + if (searchQuery != null) { + searchQuery = null; + loadData(); return; } @@ -276,78 +247,33 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker } @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); } - @Override - public void onMarkerDeleted() { - // Do nothing + private void loadData() { + viewBinding.markerListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.menu_markers))); + + List markers = contentProviderUtils.searchMarkers(trackId, searchQuery); + + adapter.swapData(markers); + + if (markers.isEmpty()) { + viewBinding.markerListEmpty.setVisibility(View.VISIBLE); + viewBinding.markerList.setVisibility(View.GONE); + } else { + viewBinding.markerListEmpty.setVisibility(View.GONE); + viewBinding.markerList.setVisibility(View.VISIBLE); + } } private void onRecordingStatusChanged(RecordingStatus status) { recordingStatus = status; } - - private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks { - - private String searchQuery = null; - - public String getSearchQuery() { - return searchQuery; - } - - public void setSearch(String searchQuery) { - this.searchQuery = searchQuery; - restart(); - viewBinding.markerListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.menu_markers))); - } - - public void restart() { - LoaderManager.getInstance(MarkerListActivity.this).restartLoader(0, null, loaderCallbacks); - } - - @NonNull - @Override - public Loader onCreateLoader(int arg0, Bundle arg1) { - final String[] PROJECTION = new String[]{MarkerColumns._ID, - MarkerColumns.NAME, MarkerColumns.DESCRIPTION, MarkerColumns.CATEGORY, - MarkerColumns.TIME, MarkerColumns.PHOTOURL, MarkerColumns.TRACKID}; - - if (searchQuery == null) { - if (track != null) { - return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, MarkerColumns.TRACKID + "=?", new String[]{String.valueOf(track.getId().id())}, null); - } else { - return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, null, null, null); - } - } else { - final String SEARCH_QUERY = MarkerColumns.NAME + " LIKE ? OR " + - MarkerColumns.DESCRIPTION + " LIKE ? OR " + - MarkerColumns.CATEGORY + " LIKE ?"; - final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"}; - return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, MarkerColumns.DEFAULT_SORT_ORDER + " DESC"); - } - } - - @Override - public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { - resourceCursorAdapter.swapCursor(cursor); - } - - @Override - public void onLoaderReset(@NonNull Loader loader) { - resourceCursorAdapter.swapCursor(null); - } - } } diff --git a/src/main/java/de/dennisguse/opentracks/ui/markers/MarkerResourceCursorAdapter.java b/src/main/java/de/dennisguse/opentracks/ui/markers/MarkerResourceCursorAdapter.java deleted file mode 100644 index 103aa7c10..000000000 --- a/src/main/java/de/dennisguse/opentracks/ui/markers/MarkerResourceCursorAdapter.java +++ /dev/null @@ -1,195 +0,0 @@ -package de.dennisguse.opentracks.ui.markers; - -import android.app.Activity; -import android.content.Context; -import android.content.res.TypedArray; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.ThumbnailUtils; -import android.net.Uri; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.collection.LruCache; -import androidx.cursoradapter.widget.ResourceCursorAdapter; - -import java.io.IOException; -import java.io.InputStream; -import java.time.Instant; -import java.time.OffsetDateTime; - -import de.dennisguse.opentracks.R; -import de.dennisguse.opentracks.data.ContentProviderUtils; -import de.dennisguse.opentracks.data.models.Track; -import de.dennisguse.opentracks.data.tables.MarkerColumns; -import de.dennisguse.opentracks.ui.util.ExecutorListViewService; -import de.dennisguse.opentracks.ui.util.ListItemUtils; -import de.dennisguse.opentracks.ui.util.ScrollVisibleViews; - -public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implements ScrollVisibleViews.VisibleViewsListener { - - private static final String TAG = MarkerResourceCursorAdapter.class.getSimpleName(); - - private static final int LIST_PREFERRED_ITEM_HEIGHT_DEFAULT = 128; - - private final Activity activity; - - //TODO Should be Marker.Id - private final ExecutorListViewService executorService = new ExecutorListViewService<>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT); - - private boolean scroll = false; - - // Cache size is in bytes. - private final LruCache memoryCache; - - public MarkerResourceCursorAdapter(Activity activity, int layout) { - super(activity, layout, null, 0); - - this.activity = activity; - - // Get max available VM memory, exceeding this amount will throw an OutOfMemory exception. - final long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); - final int cacheSize = (int) (Runtime.getRuntime().maxMemory() - usedMemory) / 8; - - memoryCache = new LruCache<>(cacheSize) { - @Override - protected int sizeOf(@NonNull String key, @NonNull Bitmap bitmap) { - return bitmap.getByteCount(); - } - }; - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - int idIndex = cursor.getColumnIndex(MarkerColumns._ID); - int nameIndex = cursor.getColumnIndex(MarkerColumns.NAME); - int timeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.TIME); - int categoryIndex = cursor.getColumnIndex(MarkerColumns.CATEGORY); - int descriptionIndex = cursor.getColumnIndex(MarkerColumns.DESCRIPTION); - int photoUrlIndex = cursor.getColumnIndex(MarkerColumns.PHOTOURL); - int trackIdIndex = cursor.getColumnIndex(MarkerColumns.TRACKID); - - long id = cursor.getLong(idIndex); - int iconId = MarkerUtils.ICON_ID; - String name = cursor.getString(nameIndex); - long time = cursor.getLong(timeIndex); - String category = cursor.getString(categoryIndex); - String description = cursor.getString(descriptionIndex); - String photoUrl = cursor.getString(photoUrlIndex); - long trackId = cursor.getLong(trackIdIndex); - - view.setTag(String.valueOf(id)); - - boolean hasPhoto = photoUrl != null && !photoUrl.equals(""); - - ImageView imageView = view.findViewById(R.id.list_item_photo); - ImageView textGradient = view.findViewById(R.id.list_item_text_gradient); - imageView.setVisibility(hasPhoto ? View.VISIBLE : View.GONE); - textGradient.setVisibility(hasPhoto ? View.VISIBLE : View.GONE); - imageView.setImageBitmap(null); - - if (hasPhoto) { - int height = getPhotoHeight(activity); - ViewGroup.LayoutParams params = imageView.getLayoutParams(); - params.height = height; - imageView.setLayoutParams(params); - - if (getBitmapFromMemCache(String.valueOf(id)) != null || !scroll) { - asyncLoadPhoto(view, imageView, photoUrl, id); - } - } - - ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); - Track track = contentProviderUtils.getTrack(new Track.Id(trackId)); - ListItemUtils.setListItem(activity, view, false, iconId, R.string.image_marker, name, null, null, 0, - OffsetDateTime.ofInstant(Instant.ofEpochMilli(time), track.getZoneOffset()), category, description, hasPhoto); - } - - public void clear() { - executorService.shutdown(); - } - - @Override - public void onViewVisible(View view, int position) { - scroll = true; - - Cursor cursor = getCursor(); - if (!cursor.moveToPosition(position)) { - return; - } - - long id = cursor.getLong(cursor.getColumnIndexOrThrow(MarkerColumns._ID)); - String photoUrl = cursor.getString(cursor.getColumnIndexOrThrow(MarkerColumns.PHOTOURL)); - - boolean hasPhoto = photoUrl != null && !photoUrl.equals(""); - if (hasPhoto) { - ImageView imageView = view.findViewById(R.id.list_item_photo); - asyncLoadPhoto(view, imageView, photoUrl, id); - } - } - - public void markerInvalid(long id) { - memoryCache.remove(String.valueOf(id)); - scroll = false; - } - - private void addBitmapToMemoryCache(String key, Bitmap bitmap) { - synchronized (memoryCache) { - if (getBitmapFromMemCache(key) == null) { - memoryCache.put(key, bitmap); - } - } - } - - private Bitmap getBitmapFromMemCache(String key) { - return memoryCache.get(key); - } - - /** - * It loads the photoUrl in the imageView from view. - * It takes the photo from cache or from storage if isn't in the cache. - * - * @param view item's view. - * @param imageView view object where photo will be loaded. - * @param photoUrl photo's url. - * @param id marker's id where photo belong. - */ - private void asyncLoadPhoto(View view, ImageView imageView, String photoUrl, long id) { - Bitmap photo = getBitmapFromMemCache(String.valueOf(id)); - imageView.setImageBitmap(photo); - - if (photo == null) { - executorService.execute(id, () -> { - try (InputStream inputStream = activity.getContentResolver().openInputStream(Uri.parse(photoUrl))) { - Bitmap bitmap = BitmapFactory.decodeStream(inputStream); - int height = getPhotoHeight(activity); - Log.d(TAG, "Width : " + (bitmap.getWidth() / (bitmap.getHeight() / height)) + " | Height: " + height); - final Bitmap finalPhoto = ThumbnailUtils.extractThumbnail(bitmap, bitmap.getWidth() / (bitmap.getHeight() / height), height); - addBitmapToMemoryCache(String.valueOf(id), finalPhoto); - if (view.getTag().equals(String.valueOf(id))) { - activity.runOnUiThread(() -> imageView.setImageBitmap(finalPhoto)); - } - } catch (IOException e) { - Log.e(TAG, "Failed to image " + e); - } - }); - } - } - - /** - * Gets the photo height. - * - * @param context the context - */ - private static int getPhotoHeight(Context context) { - int[] attrs = new int[]{android.R.attr.listPreferredItemHeight}; - TypedArray typeArray = context.obtainStyledAttributes(attrs); - int height = typeArray.getDimensionPixelSize(0, LIST_PREFERRED_ITEM_HEIGHT_DEFAULT); - typeArray.recycle(); - return 2 * height; - } -} diff --git a/src/main/res/layout/marker_list.xml b/src/main/res/layout/marker_list.xml index bead2e4c6..2eecbbfaf 100644 --- a/src/main/res/layout/marker_list.xml +++ b/src/main/res/layout/marker_list.xml @@ -37,7 +37,7 @@ limitations under the License. android:paddingBottom="?attr/actionBarSize" app:layout_behavior="@string/appbar_scrolling_view_behavior"> -