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"> -