Convert playback history fragment to lazy loading (#5886)

This commit is contained in:
Paul Ganssle 2022-06-09 16:24:22 -04:00 committed by GitHub
parent fd066a648b
commit df53c5bfe5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 506 additions and 549 deletions

View File

@ -49,9 +49,9 @@ public class InboxFragment extends EpisodesListFragment implements Toolbar.OnMen
@NonNull
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View inboxContainer = View.inflate(getContext(), R.layout.inbox_fragment, null);
View inboxContainer = View.inflate(getContext(), R.layout.list_container_fragment, null);
View root = super.onCreateView(inflater, container, savedInstanceState);
((FrameLayout) inboxContainer.findViewById(R.id.inboxContent)).addView(root);
((FrameLayout) inboxContainer.findViewById(R.id.listContent)).addView(root);
emptyView.setTitle(R.string.no_inbox_head_label);
emptyView.setMessage(R.string.no_inbox_label);

View File

@ -1,57 +1,36 @@
package de.danoeh.antennapod.fragment;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.event.FeedItemEvent;
import de.danoeh.antennapod.event.playback.PlaybackHistoryEvent;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.view.EmptyViewHandler;
import de.danoeh.antennapod.view.EpisodeItemListRecyclerView;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuItemClickListener {
public class PlaybackHistoryFragment extends EpisodesListFragment implements Toolbar.OnMenuItemClickListener {
public static final String TAG = "PlaybackHistoryFragment";
private static final String KEY_UP_ARROW = "up_arrow";
private List<FeedItem> playbackHistory;
private PlaybackHistoryListAdapter adapter;
private Disposable disposable;
private EpisodeItemListRecyclerView recyclerView;
private EmptyViewHandler emptyView;
private ProgressBar progressBar;
private Toolbar toolbar;
private boolean displayUpArrow;
@ -64,8 +43,12 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.simple_list_fragment, container, false);
toolbar = root.findViewById(R.id.toolbar);
View historyContainer = View.inflate(getContext(), R.layout.list_container_fragment, null);
View root = super.onCreateView(inflater, container, savedInstanceState);
((FrameLayout) historyContainer.findViewById(R.id.listContent)).addView(root);
toolbar = historyContainer.findViewById(R.id.toolbar);
toolbar.setTitle(R.string.playback_history_label);
toolbar.setOnMenuItemClickListener(this);
displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0;
@ -76,34 +59,14 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI
toolbar.inflateMenu(R.menu.playback_history);
refreshToolbarState();
recyclerView = root.findViewById(R.id.recyclerView);
recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool());
adapter = new PlaybackHistoryListAdapter((MainActivity) getActivity());
recyclerView.setAdapter(adapter);
progressBar = root.findViewById(R.id.progLoading);
listAdapter = new PlaybackHistoryListAdapter((MainActivity) getActivity());
recyclerView.setAdapter(listAdapter);
emptyView = new EmptyViewHandler(getActivity());
emptyView.setIcon(R.drawable.ic_history);
emptyView.setTitle(R.string.no_history_head_label);
emptyView.setMessage(R.string.no_history_label);
emptyView.attachToRecyclerView(recyclerView);
return root;
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
loadItems();
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
if (disposable != null) {
disposable.dispose();
}
return historyContainer;
}
@Override
@ -112,55 +75,8 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI
super.onSaveInstanceState(outState);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(FeedItemEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
if (playbackHistory == null) {
return;
} else if (adapter == null) {
loadItems();
return;
}
for (int i = 0, size = event.items.size(); i < size; i++) {
FeedItem item = event.items.get(i);
int pos = FeedItemUtil.indexOfItemWithId(playbackHistory, item.getId());
if (pos >= 0) {
playbackHistory.remove(pos);
playbackHistory.add(pos, item);
adapter.notifyItemChangedCompat(pos);
}
}
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(DownloadEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
DownloaderUpdate update = event.update;
if (adapter != null && update.mediaIds.length > 0) {
for (long mediaId : update.mediaIds) {
int pos = FeedItemUtil.indexOfItemWithMediaId(playbackHistory, mediaId);
if (pos >= 0) {
adapter.notifyItemChangedCompat(pos);
}
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
if (adapter != null) {
for (int i = 0; i < adapter.getItemCount(); i++) {
EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i);
if (holder != null && holder.isCurrentlyPlayingItem()) {
holder.notifyPlaybackPositionUpdated(event);
break;
}
}
}
}
public void refreshToolbarState() {
boolean hasHistory = playbackHistory != null && !playbackHistory.isEmpty();
boolean hasHistory = episodes != null && !episodes.isEmpty();
toolbar.getMenu().findItem(R.id.clear_history_item).setVisible(hasHistory);
}
@ -173,33 +89,6 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI
return false;
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
FeedItem selectedItem = adapter.getLongPressedItem();
if (selectedItem == null) {
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
}
return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onKeyUp(KeyEvent event) {
if (!isAdded() || !isVisible() || !isMenuVisible()) {
return;
}
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_T:
recyclerView.smoothScrollToPosition(0);
break;
case KeyEvent.KEYCODE_B:
recyclerView.smoothScrollToPosition(adapter.getItemCount() - 1);
break;
default:
break;
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onHistoryUpdated(PlaybackHistoryEvent event) {
loadItems();
@ -212,59 +101,47 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI
refreshToolbarState();
}
@Override
@Subscribe(threadMode = ThreadMode.MAIN)
public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) {
loadItems();
refreshToolbarState();
}
private void onFragmentLoaded() {
adapter.notifyDataSetChanged();
@Override
protected void onFragmentLoaded(List<FeedItem> episodes) {
super.onFragmentLoaded(episodes);
listAdapter.notifyDataSetChanged();
refreshToolbarState();
}
private void loadItems() {
if (disposable != null) {
disposable.dispose();
}
progressBar.setVisibility(View.VISIBLE);
emptyView.hide();
disposable = Observable.fromCallable(this::loadData)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
progressBar.setVisibility(View.GONE);
playbackHistory = result;
adapter.updateItems(playbackHistory);
onFragmentLoaded();
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
@NonNull
private List<FeedItem> loadData() {
List<FeedItem> history = DBReader.getPlaybackHistory();
DBReader.loadAdditionalFeedItemListData(history);
return history;
}
private class PlaybackHistoryListAdapter extends EpisodeItemListAdapter {
public PlaybackHistoryListAdapter(MainActivity mainActivity) {
super(mainActivity);
}
@Override
protected void afterBindViewHolder(EpisodeItemViewHolder holder, int pos) {
// played items shouldn't be transparent for this fragment since, *all* items
// in this fragment will, by definition, be played. So it serves no purpose and can make
// it harder to read.
holder.itemView.setAlpha(1.0f);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
MenuItemUtils.setOnClickListeners(menu, PlaybackHistoryFragment.this::onContextItemSelected);
}
}
@NonNull
@Override
protected List<FeedItem> loadData() {
return DBReader.getPlaybackHistory(0, page * EPISODES_PER_PAGE);
}
@NonNull
@Override
protected List<FeedItem> loadMoreData(int page) {
return DBReader.getPlaybackHistory((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE);
}
@Override
protected int loadTotalItemCount() {
return (int) DBReader.getPlaybackHistoryLength();
}
}

View File

@ -9,13 +9,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
android:layout_alignParentTop="true"
app:title="@string/inbox_label" />
<FrameLayout
android:id="@+id/inboxContent"
android:id="@+id/listContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar" />

View File

@ -45,11 +45,6 @@ public final class DBReader {
private static final String TAG = "DBReader";
/**
* Maximum size of the list returned by {@link #getPlaybackHistory()}.
*/
public static final int PLAYBACK_HISTORY_SIZE = 50;
/**
* Maximum size of the list returned by {@link #getDownloadLog()}.
*/
@ -419,11 +414,12 @@ public final class DBReader {
* Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode
* has been completed at least once.
*
* @param limit The maximum number of items to return.
*
* @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order.
* The size of the returned list is limited by {@link #PLAYBACK_HISTORY_SIZE}.
*/
@NonNull
public static List<FeedItem> getPlaybackHistory() {
public static List<FeedItem> getPlaybackHistory(int offset, int limit) {
Log.d(TAG, "getPlaybackHistory() called");
PodDBAdapter adapter = PodDBAdapter.getInstance();
@ -432,7 +428,7 @@ public final class DBReader {
Cursor mediaCursor = null;
Cursor itemCursor = null;
try {
mediaCursor = adapter.getCompletedMediaCursor(PLAYBACK_HISTORY_SIZE);
mediaCursor = adapter.getCompletedMediaCursor(offset, limit);
String[] itemIds = new String[mediaCursor.getCount()];
for (int i = 0; i < itemIds.length && mediaCursor.moveToPosition(i); i++) {
int index = mediaCursor.getColumnIndex(PodDBAdapter.KEY_FEEDITEM);
@ -454,6 +450,17 @@ public final class DBReader {
}
}
public static long getPlaybackHistoryLength() {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
try {
return adapter.getCompletedMediaLength();
} finally {
adapter.close();
}
}
/**
* Loads the download log from the database.
*

View File

@ -3,6 +3,8 @@ package de.danoeh.antennapod.core.storage;
import android.content.Context;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Random;
@ -16,8 +18,11 @@ import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.RobolectricTestRunner;
import static de.danoeh.antennapod.core.storage.DbTestUtils.saveFeedlist;
@ -31,9 +36,10 @@ import static org.junit.Assert.assertTrue;
* Test class for DBReader.
*/
@SuppressWarnings("ConstantConditions")
@RunWith(RobolectricTestRunner.class)
@RunWith(Enclosed.class)
public class DbReaderTest {
@Ignore("Not a test")
public static class TestBase {
@Before
public void setUp() {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
@ -51,7 +57,10 @@ public class DbReaderTest {
PodDBAdapter.tearDownTests();
DBWriter.tearDownTests();
}
}
@RunWith(RobolectricTestRunner.class)
public static class SingleTests extends TestBase {
@Test
public void testGetFeedList() {
List<Feed> feeds = saveFeedlist(10, 0, false);
@ -289,33 +298,26 @@ public class DbReaderTest {
}
@Test
public void testGetPlaybackHistory() {
final int numItems = (DBReader.PLAYBACK_HISTORY_SIZE + 1) * 2;
final int playedItems = DBReader.PLAYBACK_HISTORY_SIZE + 1;
final int numReturnedItems = Math.min(playedItems, DBReader.PLAYBACK_HISTORY_SIZE);
final int numFeeds = 1;
public void testGetPlaybackHistoryLength() {
final int totalItems = 100;
Feed feed = DbTestUtils.saveFeedlist(numFeeds, numItems, true).get(0);
long[] ids = new long[playedItems];
Feed feed = DbTestUtils.saveFeedlist(1, totalItems, true).get(0);
PodDBAdapter adapter = PodDBAdapter.getInstance();
for (int playedItems : Arrays.asList(0, 1, 20, 100)) {
adapter.open();
for (int i = 0; i < playedItems; i++) {
for (int i = 0; i < playedItems; ++i) {
FeedMedia m = feed.getItems().get(i).getMedia();
m.setPlaybackCompletionDate(new Date(i + 1));
adapter.setFeedMediaPlaybackCompletionDate(m);
ids[ids.length - 1 - i] = m.getItem().getId();
}
adapter.close();
List<FeedItem> saved = DBReader.getPlaybackHistory();
assertNotNull(saved);
assertEquals("Wrong size: ", numReturnedItems, saved.size());
for (int i = 0; i < numReturnedItems; i++) {
FeedItem item = saved.get(i);
assertNotNull(item.getMedia().getPlaybackCompletionDate());
assertEquals("Wrong sort order: ", item.getId(), ids[i]);
long len = DBReader.getPlaybackHistoryLength();
assertEquals("Wrong size: ", (int) len, playedItems);
}
}
@Test
@ -437,4 +439,70 @@ public class DbReaderTest {
item1.getMedia().getDownload_url());
assertEquals(item1.getItemIdentifier(), feedItemByGuid.getItemIdentifier());
}
}
@RunWith(ParameterizedRobolectricTestRunner.class)
public static class PlaybackHistoryTest extends TestBase {
private int paramOffset;
private int paramLimit;
@ParameterizedRobolectricTestRunner.Parameters
public static Collection<Object[]> data() {
List<Integer> limits = Arrays.asList(1, 20, 100);
List<Integer> offsets = Arrays.asList(0, 10, 20);
Object[][] rv = new Object[limits.size() * offsets.size()][2];
int i = 0;
for (int offset : offsets) {
for (int limit : limits) {
rv[i][0] = offset;
rv[i][1] = limit;
i++;
}
}
return Arrays.asList(rv);
}
public PlaybackHistoryTest(int offset, int limit) {
this.paramOffset = offset;
this.paramLimit = limit;
}
@Test
public void testGetPlaybackHistory() {
final int numItems = (paramLimit + 1) * 2;
final int playedItems = paramLimit + 1;
final int numReturnedItems = Math.min(Math.max(playedItems - paramOffset, 0), paramLimit);
final int numFeeds = 1;
Feed feed = DbTestUtils.saveFeedlist(numFeeds, numItems, true).get(0);
long[] ids = new long[playedItems];
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
for (int i = 0; i < playedItems; i++) {
FeedMedia m = feed.getItems().get(i).getMedia();
m.setPlaybackCompletionDate(new Date(i + 1));
adapter.setFeedMediaPlaybackCompletionDate(m);
ids[ids.length - 1 - i] = m.getItem().getId();
}
adapter.close();
List<FeedItem> saved = DBReader.getPlaybackHistory(paramOffset, paramLimit);
assertNotNull(saved);
assertEquals(String.format("Wrong size with offset %d and limit %d: ",
paramOffset, paramLimit),
numReturnedItems, saved.size());
for (int i = 0; i < numReturnedItems; i++) {
FeedItem item = saved.get(i);
assertNotNull(item.getMedia().getPlaybackCompletionDate());
assertEquals(String.format("Wrong sort order with offset %d and limit %d: ",
paramOffset, paramLimit),
item.getId(), ids[paramOffset + i]);
}
}
}
}

View File

@ -1076,18 +1076,23 @@ public class PodDBAdapter {
* Returns a cursor which contains feed media objects with a playback
* completion date in ascending order.
*
* @param offset The row to start at.
* @param limit The maximum row count of the returned cursor. Must be an
* integer >= 0.
* @throws IllegalArgumentException if limit < 0
*/
public final Cursor getCompletedMediaCursor(int limit) {
public final Cursor getCompletedMediaCursor(int offset, int limit) {
if (limit < 0) {
throw new IllegalArgumentException("Limit must be >= 0");
}
return db.query(TABLE_NAME_FEED_MEDIA, null,
KEY_PLAYBACK_COMPLETION_DATE + " > 0", null, null,
null, String.format(Locale.US, "%s DESC LIMIT %d", KEY_PLAYBACK_COMPLETION_DATE, limit));
null, String.format(Locale.US, "%s DESC LIMIT %d, %d", KEY_PLAYBACK_COMPLETION_DATE, offset, limit));
}
public final long getCompletedMediaLength() {
return DatabaseUtils.queryNumEntries(db, TABLE_NAME_FEED_MEDIA, KEY_PLAYBACK_COMPLETION_DATE + "> 0");
}
public final Cursor getSingleFeedMediaCursor(long id) {