Merge pull request #3351 from ByteHamster/lazy-load-episodes

More episodes on all episodes
This commit is contained in:
H. Lehmann 2019-08-30 15:10:45 +02:00 committed by GitHub
commit 8a3e4f8765
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 588 additions and 485 deletions

View File

@ -56,14 +56,13 @@ import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.Flavors;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.StorageUtils;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import de.danoeh.antennapod.dialog.RatingDialog;
import de.danoeh.antennapod.dialog.RenameFeedDialog;
import de.danoeh.antennapod.fragment.AddFeedFragment;
import de.danoeh.antennapod.fragment.DownloadsFragment;
import de.danoeh.antennapod.fragment.EpisodesFragment;
import de.danoeh.antennapod.fragment.ExternalPlayerFragment;
import de.danoeh.antennapod.fragment.ItemlistFragment;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
@ -338,7 +337,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi
}
public void loadFeedFragmentById(long feedId, Bundle args) {
Fragment fragment = ItemlistFragment.newInstance(feedId);
Fragment fragment = FeedItemlistFragment.newInstance(feedId);
if(args != null) {
fragment.setArguments(args);
}

View File

@ -14,13 +14,11 @@ import com.bumptech.glide.Glide;
import java.lang.ref.WeakReference;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.fragment.AddFeedFragment;
import de.danoeh.antennapod.fragment.ItemlistFragment;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import jp.shts.android.library.TriangleLabelView;
/**
@ -142,7 +140,7 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI
if (position == getAddTilePosition()) {
mainActivityRef.get().loadChildFragment(new AddFeedFragment());
} else {
Fragment fragment = ItemlistFragment.newInstance(getItemId(position));
Fragment fragment = FeedItemlistFragment.newInstance(getItemId(position));
mainActivityRef.get().loadChildFragment(fragment);
}
}

View File

@ -1,18 +1,9 @@
package de.danoeh.antennapod.fragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.SimpleItemAnimator;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@ -20,240 +11,48 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.joanzapata.iconify.Iconify;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedItemFilter;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.dialog.FilterDialog;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.AllEpisodesRecycleAdapter;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedItemFilter;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.dialog.FilterDialog;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.view.EmptyViewHandler;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
/**
* Shows unread or recently published episodes
* Like 'EpisodesFragment' except that it only shows new episodes and
* supports swiping to mark as read.
*/
public class AllEpisodesFragment extends Fragment {
public class AllEpisodesFragment extends EpisodesListFragment {
public static final String TAG = "AllEpisodesFragment";
private static final String PREF_NAME = "PrefAllEpisodesFragment";
private static final int EVENTS = EventDistributor.FEED_LIST_UPDATE |
EventDistributor.UNREAD_ITEMS_UPDATE |
EventDistributor.PLAYER_STATUS_UPDATE;
private static final int EPISODES_PER_PAGE = 150;
private static final int VISIBLE_EPISODES_SCROLL_THRESHOLD = 5;
private static int page = 1;
private static final int RECENT_EPISODES_LIMIT = 150;
private static final String DEFAULT_PREF_NAME = "PrefAllEpisodesFragment";
private static final String PREF_SCROLL_POSITION = "scroll_position";
private static final String PREF_SCROLL_OFFSET = "scroll_offset";
RecyclerView recyclerView;
AllEpisodesRecycleAdapter listAdapter;
private ProgressBar progLoading;
EmptyViewHandler emptyView;
@NonNull
List<FeedItem> episodes = new ArrayList<>();
@NonNull
private List<Downloader> downloaderList = new ArrayList<>();
private boolean isUpdatingFeeds;
boolean isMenuInvalidationAllowed = false;
Disposable disposable;
private LinearLayoutManager layoutManager;
protected TextView txtvInformation;
private static FeedItemFilter feedItemFilter = new FeedItemFilter("");
boolean showOnlyNewEpisodes() {
@Override
protected boolean showOnlyNewEpisodes() {
return false;
}
String getPrefName() {
return DEFAULT_PREF_NAME;
}
@Override
public void onStart() {
super.onStart();
setHasOptionsMenu(true);
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
loadItems();
}
@Override
public void onResume() {
super.onResume();
registerForContextMenu(recyclerView);
}
@Override
public void onPause() {
super.onPause();
saveScrollPosition();
unregisterForContextMenu(recyclerView);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
EventDistributor.getInstance().unregister(contentUpdate);
if (disposable != null) {
disposable.dispose();
}
}
private void saveScrollPosition() {
int firstItem = layoutManager.findFirstVisibleItemPosition();
View firstItemView = layoutManager.findViewByPosition(firstItem);
float topOffset;
if (firstItemView == null) {
topOffset = 0;
} else {
topOffset = firstItemView.getTop();
}
SharedPreferences prefs = getActivity().getSharedPreferences(getPrefName(), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PREF_SCROLL_POSITION, firstItem);
editor.putFloat(PREF_SCROLL_OFFSET, topOffset);
editor.commit();
}
private void restoreScrollPosition() {
SharedPreferences prefs = getActivity().getSharedPreferences(getPrefName(), Context.MODE_PRIVATE);
int position = prefs.getInt(PREF_SCROLL_POSITION, 0);
float offset = prefs.getFloat(PREF_SCROLL_OFFSET, 0.0f);
if (position > 0 || offset > 0) {
layoutManager.scrollToPositionWithOffset(position, (int) offset);
// restore once, then forget
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PREF_SCROLL_POSITION, 0);
editor.putFloat(PREF_SCROLL_OFFSET, 0.0f);
editor.commit();
}
}
private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker =
() -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds();
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (!isAdded()) {
return;
}
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.episodes, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem);
MenuItemUtils.adjustTextColor(getActivity(), sv);
sv.setQueryHint(getString(R.string.search_hint));
sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
sv.clearFocus();
((MainActivity) requireActivity()).loadChildFragment(SearchFragment.newInstance(s));
return true;
}
@Override
public boolean onQueryTextChange(String s) {
return false;
}
});
isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
MenuItem markAllRead = menu.findItem(R.id.mark_all_read_item);
if (markAllRead != null) {
markAllRead.setVisible(!showOnlyNewEpisodes() && !episodes.isEmpty());
}
MenuItem removeAllNewFlags = menu.findItem(R.id.remove_all_new_flags_item);
if (removeAllNewFlags != null) {
removeAllNewFlags.setVisible(showOnlyNewEpisodes() && !episodes.isEmpty());
}
protected String getPrefName() {
return PREF_NAME;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (!super.onOptionsItemSelected(item)) {
switch (item.getItemId()) {
case R.id.refresh_item:
List<Feed> feeds = ((MainActivity) getActivity()).getFeeds();
if (feeds != null) {
DBTasks.refreshAllFeeds(getActivity(), feeds);
}
return true;
case R.id.mark_all_read_item:
ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getActivity(),
R.string.mark_all_read_label,
R.string.mark_all_read_confirmation_msg) {
@Override
public void onConfirmButtonPressed(DialogInterface dialog) {
dialog.dismiss();
DBWriter.markAllItemsRead();
Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show();
}
};
markAllReadConfirmationDialog.createNewDialog().show();
return true;
case R.id.remove_all_new_flags_item:
ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getActivity(),
R.string.remove_all_new_flags_label,
R.string.remove_all_new_flags_confirmation_msg) {
@Override
public void onConfirmButtonPressed(DialogInterface dialog) {
dialog.dismiss();
DBWriter.removeAllNewFlags();
Toast.makeText(getActivity(), R.string.removed_all_new_flags_msg, Toast.LENGTH_SHORT).show();
}
};
removeAllNewFlagsConfirmationDialog.createNewDialog().show();
return true;
case R.id.filter_items:
showFilterDialog();
return true;
@ -263,81 +62,62 @@ public class AllEpisodesFragment extends Fragment {
} else {
return true;
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
Log.d(TAG, "onContextItemSelected() called with: " + "item = [" + item + "]");
if (!getUserVisibleHint()) {
return false;
}
if (!isVisible()) {
return false;
}
if (item.getItemId() == R.id.share_item) {
return true; // avoids that the position is reset when we need it in the submenu
}
if (listAdapter.getSelectedItem() == null) {
Log.i(TAG, "Selected item or listAdapter was null, ignoring selection");
return super.onContextItemSelected(item);
}
FeedItem selectedItem = listAdapter.getSelectedItem();
// Remove new flag contains UI logic specific to All/New/FavoriteSegments,
// e.g., Undo with Snackbar,
// and is handled by this class rather than the generic FeedItemMenuHandler
// Undo is useful for Remove new flag, given there is no UI to undo it otherwise,
// i.e., there is context menu item for Mark as new
if (R.id.remove_new_flag_item == item.getItemId()) {
removeNewFlagWithUndo(selectedItem);
return true;
}
return FeedItemMenuHandler.onMenuItemClicked(getActivity(), item.getItemId(), selectedItem);
}
@NonNull
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View root = inflater.inflate(R.layout.all_episodes_fragment, container, false);
txtvInformation = root.findViewById(R.id.txtvInformation);
View root = super.onCreateView(inflater, container, savedInstanceState);
layoutManager = new LinearLayoutManager(getActivity());
recyclerView = root.findViewById(android.R.id.list);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setHasFixedSize(true);
recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build());
recyclerView.setVisibility(View.GONE);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
/* Total number of episodes after last load */
private int previousTotalEpisodes = 0;
progLoading = root.findViewById(R.id.progLoading);
progLoading.setVisibility(View.VISIBLE);
/* True if loading more episodes is still in progress */
private boolean isLoadingMore = true;
emptyView = new EmptyViewHandler(getContext());
emptyView.attachToRecyclerView(recyclerView);
emptyView.setIcon(R.attr.feed);
emptyView.setTitle(R.string.no_all_episodes_head_label);
emptyView.setMessage(R.string.no_all_episodes_label);
@Override
public void onScrolled(RecyclerView recyclerView, int deltaX, int deltaY) {
super.onScrolled(recyclerView, deltaX, deltaY);
createRecycleAdapter(recyclerView, emptyView);
emptyView.hide();
int visibleEpisodeCount = recyclerView.getChildCount();
int totalEpisodeCount = recyclerView.getLayoutManager().getItemCount();
int firstVisibleEpisode = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
/* Determine if loading more episodes has finished */
if (isLoadingMore) {
if (totalEpisodeCount > previousTotalEpisodes) {
isLoadingMore = false;
previousTotalEpisodes = totalEpisodeCount;
}
}
/* Determine if the user scrolled to the bottom and loading more episodes is not already in progress */
if (!isLoadingMore && (totalEpisodeCount - visibleEpisodeCount)
<= (firstVisibleEpisode + VISIBLE_EPISODES_SCROLL_THRESHOLD)) {
/* The end of the list has been reached. Load more data. */
page++;
loadMoreItems();
isLoadingMore = true;
}
}
});
return root;
}
protected void onFragmentLoaded(List<FeedItem> episodes) {
listAdapter.notifyDataSetChanged();
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
menu.findItem(R.id.filter_items).setVisible(true);
menu.findItem(R.id.mark_all_read_item).setVisible(!episodes.isEmpty());
}
if (episodes.size() == 0) {
createRecycleAdapter(recyclerView, emptyView);
}
@Override
protected void onFragmentLoaded(List<FeedItem> episodes) {
super.onFragmentLoaded(episodes);
if (feedItemFilter.getValues().length > 0) {
txtvInformation.setText("{fa-info-circle} " + this.getString(R.string.filtered_label));
@ -346,179 +126,22 @@ public class AllEpisodesFragment extends Fragment {
} else {
txtvInformation.setVisibility(View.GONE);
}
restoreScrollPosition();
requireActivity().invalidateOptionsMenu();
}
/**
* Currently, we need to recreate the list adapter in order to be able to undo last item via the
* snackbar. See #3084 for details.
*/
private void createRecycleAdapter(RecyclerView recyclerView, EmptyViewHandler emptyViewHandler) {
MainActivity mainActivity = (MainActivity) getActivity();
listAdapter = new AllEpisodesRecycleAdapter(mainActivity, itemAccess, showOnlyNewEpisodes());
listAdapter.setHasStableIds(true);
recyclerView.setAdapter(listAdapter);
emptyViewHandler.updateAdapter(listAdapter);
}
private final AllEpisodesRecycleAdapter.ItemAccess itemAccess = new AllEpisodesRecycleAdapter.ItemAccess() {
@Override
public int getCount() {
return episodes.size();
}
@Override
public FeedItem getItem(int position) {
if (0 <= position && position < episodes.size()) {
return episodes.get(position);
}
return null;
}
@Override
public LongList getItemsIds() {
LongList ids = new LongList(episodes.size());
for (FeedItem episode : episodes) {
ids.add(episode.getId());
}
return ids;
}
@Override
public int getItemDownloadProgressPercent(FeedItem item) {
for (Downloader downloader : downloaderList) {
DownloadRequest downloadRequest = downloader.getDownloadRequest();
if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA
&& downloadRequest.getFeedfileId() == item.getMedia().getId()) {
return downloadRequest.getProgressPercent();
}
}
return 0;
}
@Override
public boolean isInQueue(FeedItem item) {
return item != null && item.isTagged(FeedItem.TAG_QUEUE);
}
@Override
public LongList getQueueIds() {
LongList queueIds = new LongList();
for (FeedItem item : episodes) {
if (item.isTagged(FeedItem.TAG_QUEUE)) {
queueIds.add(item.getId());
}
}
return queueIds;
}
};
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(FeedItemEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
for (FeedItem item : event.items) {
int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId());
if (pos >= 0) {
episodes.remove(pos);
if (shouldUpdatedItemRemainInList(item)) {
episodes.add(pos, item);
listAdapter.notifyItemChanged(pos);
} else {
listAdapter.notifyItemRemoved(pos);
}
}
}
}
protected boolean shouldUpdatedItemRemainInList(FeedItem item) {
return true;
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(DownloadEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
DownloaderUpdate update = event.update;
downloaderList = update.downloaders;
if (isMenuInvalidationAllowed && isUpdatingFeeds != update.feedIds.length > 0) {
requireActivity().invalidateOptionsMenu();
}
if (update.mediaIds.length > 0) {
for (long mediaId : update.mediaIds) {
int pos = FeedItemUtil.indexOfItemWithMediaId(episodes, mediaId);
if (pos >= 0) {
listAdapter.notifyItemChanged(pos);
}
}
}
}
private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
@Override
public void update(EventDistributor eventDistributor, Integer arg) {
if ((arg & EVENTS) != 0) {
loadItems();
if (isUpdatingFeeds != updateRefreshMenuItemChecker.isRefreshing()) {
requireActivity().invalidateOptionsMenu();
}
}
}
};
void loadItems() {
private void loadMoreItems() {
if (disposable != null) {
disposable.dispose();
}
disposable = Observable.fromCallable(this::loadData)
disposable = Observable.fromCallable(this::loadMoreData)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(data -> {
progLoading.setVisibility(View.GONE);
episodes = data;
episodes.addAll(data);
onFragmentLoaded(episodes);
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
@NonNull
List<FeedItem> loadData() {
return feedItemFilter.filter( DBReader.getRecentlyPublishedEpisodes(RECENT_EPISODES_LIMIT) );
}
void removeNewFlagWithUndo(FeedItem item) {
if (item == null) {
return;
}
Log.d(TAG, "removeNewFlagWithUndo(" + item.getId() + ")");
if (disposable != null) {
disposable.dispose();
}
// we're marking it as unplayed since the user didn't actually play it
// but they don't want it considered 'NEW' anymore
DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId());
final Handler h = new Handler(getActivity().getMainLooper());
final Runnable r = () -> {
FeedMedia media = item.getMedia();
if (media != null && media.hasAlmostEnded() && UserPreferences.isAutoDelete()) {
DBWriter.deleteFeedMediaOfItem(getActivity(), media.getId());
}
};
Snackbar snackbar = Snackbar.make(getView(), getString(R.string.removed_new_flag_label),
Snackbar.LENGTH_LONG);
snackbar.setAction(getString(R.string.undo), v -> {
DBWriter.markItemPlayed(FeedItem.NEW, item.getId());
// don't forget to cancel the thing that's going to remove the media
h.removeCallbacks(r);
});
snackbar.show();
h.postDelayed(r, (int) Math.ceil(snackbar.getDuration() * 1.05f));
}
private void showFilterDialog() {
FilterDialog filterDialog = new FilterDialog(getContext(), feedItemFilter) {
@Override
@ -530,4 +153,14 @@ public class AllEpisodesFragment extends Fragment {
filterDialog.openDialog();
}
@NonNull
@Override
protected List<FeedItem> loadData() {
return feedItemFilter.filter( DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE));
}
List<FeedItem> loadMoreData() {
return feedItemFilter.filter( DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE));
}
}

View File

@ -79,7 +79,7 @@ public class EpisodesFragment extends Fragment {
public static class EpisodesPagerAdapter extends FragmentPagerAdapter {
private final Resources resources;
private final AllEpisodesFragment[] fragments = {
private final EpisodesListFragment[] fragments = {
new NewEpisodesFragment(),
new AllEpisodesFragment(),
new FavoriteEpisodesFragment()

View File

@ -0,0 +1,488 @@
package de.danoeh.antennapod.fragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.SimpleItemAnimator;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.joanzapata.iconify.Iconify;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.AllEpisodesRecycleAdapter;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedItemFilter;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.dialog.FilterDialog;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.view.EmptyViewHandler;
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.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Shows unread or recently published episodes
*/
public abstract class EpisodesListFragment extends Fragment {
public static final String TAG = "EpisodesListFragment";
private static final int EVENTS = EventDistributor.FEED_LIST_UPDATE |
EventDistributor.UNREAD_ITEMS_UPDATE |
EventDistributor.PLAYER_STATUS_UPDATE;
private static final String DEFAULT_PREF_NAME = "PrefAllEpisodesFragment";
private static final String PREF_SCROLL_POSITION = "scroll_position";
private static final String PREF_SCROLL_OFFSET = "scroll_offset";
RecyclerView recyclerView;
AllEpisodesRecycleAdapter listAdapter;
ProgressBar progLoading;
EmptyViewHandler emptyView;
@NonNull
List<FeedItem> episodes = new ArrayList<>();
@NonNull
private List<Downloader> downloaderList = new ArrayList<>();
private boolean isUpdatingFeeds;
boolean isMenuInvalidationAllowed = false;
protected Disposable disposable;
private LinearLayoutManager layoutManager;
protected TextView txtvInformation;
boolean showOnlyNewEpisodes() {
return false;
}
String getPrefName() {
return DEFAULT_PREF_NAME;
}
@Override
public void onStart() {
super.onStart();
setHasOptionsMenu(true);
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
loadItems();
}
@Override
public void onResume() {
super.onResume();
registerForContextMenu(recyclerView);
}
@Override
public void onPause() {
super.onPause();
saveScrollPosition();
unregisterForContextMenu(recyclerView);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
EventDistributor.getInstance().unregister(contentUpdate);
if (disposable != null) {
disposable.dispose();
}
}
private void saveScrollPosition() {
int firstItem = layoutManager.findFirstVisibleItemPosition();
View firstItemView = layoutManager.findViewByPosition(firstItem);
float topOffset;
if (firstItemView == null) {
topOffset = 0;
} else {
topOffset = firstItemView.getTop();
}
SharedPreferences prefs = getActivity().getSharedPreferences(getPrefName(), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PREF_SCROLL_POSITION, firstItem);
editor.putFloat(PREF_SCROLL_OFFSET, topOffset);
editor.commit();
}
private void restoreScrollPosition() {
SharedPreferences prefs = getActivity().getSharedPreferences(getPrefName(), Context.MODE_PRIVATE);
int position = prefs.getInt(PREF_SCROLL_POSITION, 0);
float offset = prefs.getFloat(PREF_SCROLL_OFFSET, 0.0f);
if (position > 0 || offset > 0) {
layoutManager.scrollToPositionWithOffset(position, (int) offset);
// restore once, then forget
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PREF_SCROLL_POSITION, 0);
editor.putFloat(PREF_SCROLL_OFFSET, 0.0f);
editor.commit();
}
}
private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker =
() -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds();
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (!isAdded()) {
return;
}
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.episodes, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem);
MenuItemUtils.adjustTextColor(getActivity(), sv);
sv.setQueryHint(getString(R.string.search_hint));
sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
sv.clearFocus();
((MainActivity) requireActivity()).loadChildFragment(SearchFragment.newInstance(s));
return true;
}
@Override
public boolean onQueryTextChange(String s) {
return false;
}
});
isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (!super.onOptionsItemSelected(item)) {
switch (item.getItemId()) {
case R.id.refresh_item:
List<Feed> feeds = ((MainActivity) getActivity()).getFeeds();
if (feeds != null) {
DBTasks.refreshAllFeeds(getActivity(), feeds);
}
return true;
case R.id.mark_all_read_item:
ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getActivity(),
R.string.mark_all_read_label,
R.string.mark_all_read_confirmation_msg) {
@Override
public void onConfirmButtonPressed(DialogInterface dialog) {
dialog.dismiss();
DBWriter.markAllItemsRead();
Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show();
}
};
markAllReadConfirmationDialog.createNewDialog().show();
return true;
case R.id.remove_all_new_flags_item:
ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getActivity(),
R.string.remove_all_new_flags_label,
R.string.remove_all_new_flags_confirmation_msg) {
@Override
public void onConfirmButtonPressed(DialogInterface dialog) {
dialog.dismiss();
DBWriter.removeAllNewFlags();
Toast.makeText(getActivity(), R.string.removed_all_new_flags_msg, Toast.LENGTH_SHORT).show();
}
};
removeAllNewFlagsConfirmationDialog.createNewDialog().show();
return true;
default:
return false;
}
} else {
return true;
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
Log.d(TAG, "onContextItemSelected() called with: " + "item = [" + item + "]");
if (!getUserVisibleHint()) {
return false;
}
if (!isVisible()) {
return false;
}
if (item.getItemId() == R.id.share_item) {
return true; // avoids that the position is reset when we need it in the submenu
}
if (listAdapter.getSelectedItem() == null) {
Log.i(TAG, "Selected item or listAdapter was null, ignoring selection");
return super.onContextItemSelected(item);
}
FeedItem selectedItem = listAdapter.getSelectedItem();
// Remove new flag contains UI logic specific to All/New/FavoriteSegments,
// e.g., Undo with Snackbar,
// and is handled by this class rather than the generic FeedItemMenuHandler
// Undo is useful for Remove new flag, given there is no UI to undo it otherwise,
// i.e., there is context menu item for Mark as new
if (R.id.remove_new_flag_item == item.getItemId()) {
removeNewFlagWithUndo(selectedItem);
return true;
}
return FeedItemMenuHandler.onMenuItemClicked(getActivity(), item.getItemId(), selectedItem);
}
@NonNull
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View root = inflater.inflate(R.layout.all_episodes_fragment, container, false);
txtvInformation = root.findViewById(R.id.txtvInformation);
layoutManager = new LinearLayoutManager(getActivity());
recyclerView = root.findViewById(android.R.id.list);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setHasFixedSize(true);
recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build());
recyclerView.setVisibility(View.GONE);
RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
progLoading = root.findViewById(R.id.progLoading);
progLoading.setVisibility(View.VISIBLE);
emptyView = new EmptyViewHandler(getContext());
emptyView.attachToRecyclerView(recyclerView);
emptyView.setIcon(R.attr.feed);
emptyView.setTitle(R.string.no_all_episodes_head_label);
emptyView.setMessage(R.string.no_all_episodes_label);
createRecycleAdapter(recyclerView, emptyView);
emptyView.hide();
return root;
}
protected void onFragmentLoaded(List<FeedItem> episodes) {
listAdapter.notifyDataSetChanged();
if (episodes.size() == 0) {
createRecycleAdapter(recyclerView, emptyView);
}
restoreScrollPosition();
requireActivity().invalidateOptionsMenu();
}
/**
* Currently, we need to recreate the list adapter in order to be able to undo last item via the
* snackbar. See #3084 for details.
*/
private void createRecycleAdapter(RecyclerView recyclerView, EmptyViewHandler emptyViewHandler) {
MainActivity mainActivity = (MainActivity) getActivity();
listAdapter = new AllEpisodesRecycleAdapter(mainActivity, itemAccess, showOnlyNewEpisodes());
listAdapter.setHasStableIds(true);
recyclerView.setAdapter(listAdapter);
emptyViewHandler.updateAdapter(listAdapter);
}
private final AllEpisodesRecycleAdapter.ItemAccess itemAccess = new AllEpisodesRecycleAdapter.ItemAccess() {
@Override
public int getCount() {
return episodes.size();
}
@Override
public FeedItem getItem(int position) {
if (0 <= position && position < episodes.size()) {
return episodes.get(position);
}
return null;
}
@Override
public LongList getItemsIds() {
LongList ids = new LongList(episodes.size());
for (FeedItem episode : episodes) {
ids.add(episode.getId());
}
return ids;
}
@Override
public int getItemDownloadProgressPercent(FeedItem item) {
for (Downloader downloader : downloaderList) {
DownloadRequest downloadRequest = downloader.getDownloadRequest();
if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA
&& downloadRequest.getFeedfileId() == item.getMedia().getId()) {
return downloadRequest.getProgressPercent();
}
}
return 0;
}
@Override
public boolean isInQueue(FeedItem item) {
return item != null && item.isTagged(FeedItem.TAG_QUEUE);
}
@Override
public LongList getQueueIds() {
LongList queueIds = new LongList();
for (FeedItem item : episodes) {
if (item.isTagged(FeedItem.TAG_QUEUE)) {
queueIds.add(item.getId());
}
}
return queueIds;
}
};
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(FeedItemEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
for (FeedItem item : event.items) {
int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId());
if (pos >= 0) {
episodes.remove(pos);
if (shouldUpdatedItemRemainInList(item)) {
episodes.add(pos, item);
listAdapter.notifyItemChanged(pos);
} else {
listAdapter.notifyItemRemoved(pos);
}
}
}
}
protected boolean shouldUpdatedItemRemainInList(FeedItem item) {
return true;
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(DownloadEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
DownloaderUpdate update = event.update;
downloaderList = update.downloaders;
if (isMenuInvalidationAllowed && isUpdatingFeeds != update.feedIds.length > 0) {
requireActivity().invalidateOptionsMenu();
}
if (update.mediaIds.length > 0) {
for (long mediaId : update.mediaIds) {
int pos = FeedItemUtil.indexOfItemWithMediaId(episodes, mediaId);
if (pos >= 0) {
listAdapter.notifyItemChanged(pos);
}
}
}
}
private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
@Override
public void update(EventDistributor eventDistributor, Integer arg) {
if ((arg & EVENTS) != 0) {
loadItems();
if (isUpdatingFeeds != updateRefreshMenuItemChecker.isRefreshing()) {
requireActivity().invalidateOptionsMenu();
}
}
}
};
void loadItems() {
if (disposable != null) {
disposable.dispose();
}
disposable = Observable.fromCallable(this::loadData)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(data -> {
progLoading.setVisibility(View.GONE);
episodes = data;
onFragmentLoaded(episodes);
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
@NonNull
protected abstract List<FeedItem> loadData();
void removeNewFlagWithUndo(FeedItem item) {
if (item == null) {
return;
}
Log.d(TAG, "removeNewFlagWithUndo(" + item.getId() + ")");
if (disposable != null) {
disposable.dispose();
}
// we're marking it as unplayed since the user didn't actually play it
// but they don't want it considered 'NEW' anymore
DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId());
final Handler h = new Handler(getActivity().getMainLooper());
final Runnable r = () -> {
FeedMedia media = item.getMedia();
if (media != null && media.hasAlmostEnded() && UserPreferences.isAutoDelete()) {
DBWriter.deleteFeedMediaOfItem(getActivity(), media.getId());
}
};
Snackbar snackbar = Snackbar.make(getView(), getString(R.string.removed_new_flag_label),
Snackbar.LENGTH_LONG);
snackbar.setAction(getString(R.string.undo), v -> {
DBWriter.markItemPlayed(FeedItem.NEW, item.getId());
// don't forget to cancel the thing that's going to remove the media
h.removeCallbacks(r);
});
snackbar.show();
h.postDelayed(r, (int) Math.ceil(snackbar.getDuration() * 1.05f));
}
}

View File

@ -26,7 +26,7 @@ import de.danoeh.antennapod.core.storage.DBWriter;
* Like 'EpisodesFragment' except that it only shows favorite episodes and
* supports swiping to remove from favorites.
*/
public class FavoriteEpisodesFragment extends AllEpisodesFragment {
public class FavoriteEpisodesFragment extends EpisodesListFragment {
private static final String TAG = "FavoriteEpisodesFrag";
private static final String PREF_NAME = "PrefFavoriteEpisodesFragment";
@ -85,19 +85,6 @@ public class FavoriteEpisodesFragment extends AllEpisodesFragment {
return root;
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.removeItem(R.id.filter_items);
}
@Override
protected void onFragmentLoaded(List<FeedItem> episodes) {
super.onFragmentLoaded(episodes);
txtvInformation.setVisibility(View.GONE);
}
@NonNull
@Override
protected List<FeedItem> loadData() {

View File

@ -79,7 +79,7 @@ import io.reactivex.schedulers.Schedulers;
* Displays a list of FeedItems.
*/
@SuppressLint("ValidFragment")
public class ItemlistFragment extends ListFragment {
public class FeedItemlistFragment extends ListFragment {
private static final String TAG = "ItemlistFragment";
private static final int EVENTS = EventDistributor.UNREAD_ITEMS_UPDATE
@ -120,8 +120,8 @@ public class ItemlistFragment extends ListFragment {
* @param feedId The id of the feed to show
* @return the newly created instance of an ItemlistFragment
*/
public static ItemlistFragment newInstance(long feedId) {
ItemlistFragment i = new ItemlistFragment();
public static FeedItemlistFragment newInstance(long feedId) {
FeedItemlistFragment i = new FeedItemlistFragment();
Bundle b = new Bundle();
b.putLong(ARGUMENT_FEED_ID, feedId);
i.setArguments(b);

View File

@ -534,7 +534,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture {
}
private void openPodcast() {
Fragment fragment = ItemlistFragment.newInstance(item.getFeedId());
Fragment fragment = FeedItemlistFragment.newInstance(item.getFeedId());
((MainActivity)getActivity()).loadChildFragment(fragment);
}

View File

@ -6,6 +6,7 @@ import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@ -20,7 +21,7 @@ import de.danoeh.antennapod.core.storage.DBReader;
* Like 'EpisodesFragment' except that it only shows new episodes and
* supports swiping to mark as read.
*/
public class NewEpisodesFragment extends AllEpisodesFragment {
public class NewEpisodesFragment extends EpisodesListFragment {
public static final String TAG = "NewEpisodesFragment";
private static final String PREF_NAME = "PrefNewEpisodesFragment";
@ -40,6 +41,12 @@ public class NewEpisodesFragment extends AllEpisodesFragment {
return item.isNew();
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.findItem(R.id.remove_all_new_flags_item).setVisible(!episodes.isEmpty());
}
@NonNull
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -93,19 +100,6 @@ public class NewEpisodesFragment extends AllEpisodesFragment {
return root;
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.removeItem(R.id.filter_items);
}
@Override
protected void onFragmentLoaded(List<FeedItem> episodes) {
super.onFragmentLoaded(episodes);
txtvInformation.setVisibility(View.GONE);
}
@NonNull
@Override
protected List<FeedItem> loadData() {

View File

@ -22,6 +22,7 @@
android:icon="?attr/ic_filter"
android:menuCategory="container"
android:title="@string/filter"
android:visible="false"
custom:showAsAction="ifRoom"/>
<item
@ -29,6 +30,7 @@
android:title="@string/mark_all_read_label"
android:menuCategory="container"
custom:showAsAction="collapseActionView"
android:visible="false"
android:icon="?attr/navigation_accept"/>
<item
@ -36,6 +38,7 @@
android:title="@string/remove_all_new_flags_label"
android:menuCategory="container"
custom:showAsAction="collapseActionView"
android:visible="false"
android:icon="?attr/navigation_accept"/>
</menu>

View File

@ -418,17 +418,18 @@ public final class DBReader {
/**
* Loads a list of FeedItems sorted by pubDate in descending order.
*
* @param offset The first episode that should be loaded.
* @param limit The maximum number of episodes that should be loaded.
*/
@NonNull
public static List<FeedItem> getRecentlyPublishedEpisodes(int limit) {
Log.d(TAG, "getRecentlyPublishedEpisodes() called with: " + "limit = [" + limit + "]");
public static List<FeedItem> getRecentlyPublishedEpisodes(int offset, int limit) {
Log.d(TAG, "getRecentlyPublishedEpisodes() called with: " + "offset = [" + offset + "]" + " limit = [" + limit + "]" );
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor cursor = null;
try {
cursor = adapter.getRecentlyPublishedItemsCursor(limit);
cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit);
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
loadAdditionalFeedItemListData(items);
return items;

View File

@ -1020,8 +1020,8 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
public final Cursor getRecentlyPublishedItemsCursor(int limit) {
return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, null, null, null, null, KEY_PUBDATE + " DESC LIMIT " + limit);
public final Cursor getRecentlyPublishedItemsCursor(int offset, int limit) {
return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, null, null, null, null, KEY_PUBDATE + " DESC LIMIT " + offset + ", " + limit);
}
public Cursor getDownloadedItemsCursor() {