package de.danoeh.antennapod.fragment; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; 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.support.v7.widget.helper.ItemTouchHelper; 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.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; import de.danoeh.antennapod.adapter.QueueRecyclerAdapter; 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.event.QueueEvent; 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.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; 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.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.QueueSorter; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.menuhandler.MenuItemUtils; import de.greenrobot.event.EventBus; import rx.Observable; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; /** * Shows all items in the queue */ public class QueueFragment extends Fragment { public static final String TAG = "QueueFragment"; private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.UNREAD_ITEMS_UPDATE | // sent when playback position is reset EventDistributor.PLAYER_STATUS_UPDATE; private TextView infoBar; private RecyclerView recyclerView; private QueueRecyclerAdapter recyclerAdapter; private TextView txtvEmpty; private ProgressBar progLoading; private List<FeedItem> queue; private List<Downloader> downloaderList; private boolean isUpdatingFeeds = false; private static final String PREFS = "QueueFragment"; private static final String PREF_SCROLL_POSITION = "scroll_position"; private static final String PREF_SCROLL_OFFSET = "scroll_offset"; private Subscription subscription; private LinearLayoutManager layoutManager; private ItemTouchHelper itemTouchHelper; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); setHasOptionsMenu(true); } @Override public void onStart() { super.onStart(); if (queue != null) { onFragmentLoaded(true); } } @Override public void onResume() { super.onResume(); recyclerView.setAdapter(recyclerAdapter); loadItems(true); EventDistributor.getInstance().register(contentUpdate); EventBus.getDefault().registerSticky(this); } @Override public void onPause() { super.onPause(); saveScrollPosition(); EventDistributor.getInstance().unregister(contentUpdate); EventBus.getDefault().unregister(this); if(subscription != null) { subscription.unsubscribe(); } } public void onEventMainThread(QueueEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); if(queue == null || recyclerAdapter == null) { return; } switch(event.action) { case ADDED: queue.add(event.position, event.item); recyclerAdapter.notifyItemInserted(event.position); break; case SET_QUEUE: queue = event.items; recyclerAdapter.notifyDataSetChanged(); break; case REMOVED: case IRREVERSIBLE_REMOVED: int position = FeedItemUtil.indexOfItemWithId(queue, event.item.getId()); queue.remove(position); recyclerAdapter.notifyItemRemoved(position); break; case CLEARED: queue.clear(); recyclerAdapter.notifyDataSetChanged(); break; case SORTED: queue = event.items; recyclerAdapter.notifyDataSetChanged(); break; case MOVED: return; } saveScrollPosition(); onFragmentLoaded(false); } public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); if(queue == null || recyclerAdapter == null) { return; } for(int i=0, size = event.items.size(); i < size; i++) { FeedItem item = event.items.get(i); int pos = FeedItemUtil.indexOfItemWithId(queue, item.getId()); if(pos >= 0) { queue.remove(pos); queue.add(pos, item); recyclerAdapter.notifyItemChanged(pos); } } } public void onEventMainThread(DownloadEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; downloaderList = update.downloaders; if (isUpdatingFeeds != update.feedIds.length > 0) { getActivity().supportInvalidateOptionsMenu(); } if (recyclerAdapter != null && update.mediaIds.length > 0) { for (long mediaId : update.mediaIds) { int pos = FeedItemUtil.indexOfItemWithMediaId(queue, mediaId); if (pos >= 0) { recyclerAdapter.notifyItemChanged(pos); } } } } 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(PREFS, 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(PREFS, 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); } } private void resetViewState() { recyclerAdapter = null; } @Override public void onDestroyView() { super.onDestroyView(); resetViewState(); } 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); if (queue != null) { inflater.inflate(R.menu.queue, 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) getActivity()).loadChildFragment(SearchFragment.newInstance(s)); return true; } @Override public boolean onQueryTextChange(String s) { return false; } }); MenuItemUtils.refreshLockItem(getActivity(), menu); 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.queue_lock: boolean newLockState = !UserPreferences.isQueueLocked(); UserPreferences.setQueueLocked(newLockState); getActivity().supportInvalidateOptionsMenu(); recyclerAdapter.setLocked(newLockState); if (newLockState) { Snackbar.make(getActivity().findViewById(R.id.content), R.string .queue_locked, Snackbar.LENGTH_SHORT).show(); } else { Snackbar.make(getActivity().findViewById(R.id.content), R.string .queue_unlocked, Snackbar.LENGTH_SHORT).show(); } return true; case R.id.refresh_item: List<Feed> feeds = ((MainActivity) getActivity()).getFeeds(); if (feeds != null) { DBTasks.refreshAllFeeds(getActivity(), feeds); } return true; case R.id.clear_queue: // make sure the user really wants to clear the queue ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), R.string.clear_queue_label, R.string.clear_queue_confirmation_msg) { @Override public void onConfirmButtonPressed( DialogInterface dialog) { dialog.dismiss(); DBWriter.clearQueue(); } }; conDialog.createNewDialog().show(); return true; case R.id.queue_sort_alpha_asc: QueueSorter.sort(getActivity(), QueueSorter.Rule.ALPHA_ASC, true); return true; case R.id.queue_sort_alpha_desc: QueueSorter.sort(getActivity(), QueueSorter.Rule.ALPHA_DESC, true); return true; case R.id.queue_sort_date_asc: QueueSorter.sort(getActivity(), QueueSorter.Rule.DATE_ASC, true); return true; case R.id.queue_sort_date_desc: QueueSorter.sort(getActivity(), QueueSorter.Rule.DATE_DESC, true); return true; case R.id.queue_sort_duration_asc: QueueSorter.sort(getActivity(), QueueSorter.Rule.DURATION_ASC, true); return true; case R.id.queue_sort_duration_desc: QueueSorter.sort(getActivity(), QueueSorter.Rule.DURATION_DESC, true); return true; default: return false; } } else { return true; } } @Override public boolean onContextItemSelected(MenuItem item) { Log.d(TAG, "onContextItemSelected() called with: " + "item = [" + item + "]"); if(!isVisible()) { return false; } FeedItem selectedItem = recyclerAdapter.getSelectedItem(); if (selectedItem == null) { Log.i(TAG, "Selected item was null, ignoring selection"); return super.onContextItemSelected(item); } switch(item.getItemId()) { case R.id.move_to_top_item: int position = FeedItemUtil.indexOfItemWithId(queue, selectedItem.getId()); queue.add(0, queue.remove(position)); recyclerAdapter.notifyItemMoved(position, 0); DBWriter.moveQueueItemToTop(selectedItem.getId(), true); return true; case R.id.move_to_bottom_item: position = FeedItemUtil.indexOfItemWithId(queue, selectedItem.getId()); queue.add(queue.size()-1, queue.remove(position)); recyclerAdapter.notifyItemMoved(position, queue.size()-1); DBWriter.moveQueueItemToBottom(selectedItem.getId(), true); return true; default: try { return FeedItemMenuHandler.onMenuItemClicked(getActivity(), item.getItemId(), selectedItem); } catch (DownloadRequestException e) { e.printStackTrace(); Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show(); return true; } } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); ((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.queue_label); View root = inflater.inflate(R.layout.queue_fragment, container, false); infoBar = (TextView) root.findViewById(R.id.info_bar); recyclerView = (RecyclerView) root.findViewById(R.id.recyclerView); RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); if (animator instanceof SimpleItemAnimator) { ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); } layoutManager = new LinearLayoutManager(getActivity()); recyclerView.setLayoutManager(layoutManager); recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build()); recyclerView.setHasFixedSize(true); registerForContextMenu(recyclerView); itemTouchHelper = new ItemTouchHelper( new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT) { @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { int from = viewHolder.getAdapterPosition(); int to = target.getAdapterPosition(); Log.d(TAG, "move(" + from + ", " + to + ")"); queue.add(to, queue.remove(from)); recyclerAdapter.notifyItemMoved(from, to); DBWriter.moveQueueItem(from, to, true); return true; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { if(subscription != null) { subscription.unsubscribe(); } final int position = viewHolder.getAdapterPosition(); Log.d(TAG, "remove(" + position + ")"); final FeedItem item = queue.get(position); final boolean isRead = item.isPlayed(); DBWriter.markItemPlayed(FeedItem.PLAYED, false, item.getId()); DBWriter.removeQueueItem(getActivity(), item, true); Snackbar snackbar = Snackbar.make(root, getString(R.string.marked_as_read_label), Snackbar.LENGTH_LONG); snackbar.setAction(getString(R.string.undo), v -> { DBWriter.addQueueItemAt(getActivity(), item.getId(), position, false); if(!isRead) { DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); } }); snackbar.show(); } @Override public boolean isLongPressDragEnabled() { return !UserPreferences.isQueueLocked(); } @Override public boolean isItemViewSwipeEnabled() { return !UserPreferences.isQueueLocked(); } @Override public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { // We only want the active item if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { if (viewHolder instanceof QueueRecyclerAdapter.ItemTouchHelperViewHolder) { QueueRecyclerAdapter.ItemTouchHelperViewHolder itemViewHolder = (QueueRecyclerAdapter.ItemTouchHelperViewHolder) viewHolder; itemViewHolder.onItemSelected(); } } super.onSelectedChanged(viewHolder, actionState); } @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); if (viewHolder instanceof QueueRecyclerAdapter.ItemTouchHelperViewHolder) { QueueRecyclerAdapter.ItemTouchHelperViewHolder itemViewHolder = (QueueRecyclerAdapter.ItemTouchHelperViewHolder) viewHolder; itemViewHolder.onItemClear(); } } } ); itemTouchHelper.attachToRecyclerView(recyclerView); txtvEmpty = (TextView) root.findViewById(android.R.id.empty); txtvEmpty.setVisibility(View.GONE); progLoading = (ProgressBar) root.findViewById(R.id.progLoading); progLoading.setVisibility(View.VISIBLE); return root; } private void onFragmentLoaded(final boolean restoreScrollPosition) { if (recyclerAdapter == null) { MainActivity activity = (MainActivity) getActivity(); recyclerAdapter = new QueueRecyclerAdapter(activity, itemAccess, new DefaultActionButtonCallback(activity), itemTouchHelper); recyclerAdapter.setHasStableIds(true); recyclerView.setAdapter(recyclerAdapter); } if(queue == null || queue.size() == 0) { recyclerView.setVisibility(View.GONE); txtvEmpty.setVisibility(View.VISIBLE); } else { txtvEmpty.setVisibility(View.GONE); recyclerView.setVisibility(View.VISIBLE); } if (restoreScrollPosition) { restoreScrollPosition(); } // we need to refresh the options menu because it sometimes // needs data that may have just been loaded. getActivity().supportInvalidateOptionsMenu(); refreshInfoBar(); } private void refreshInfoBar() { String info = queue.size() + getString(R.string.episodes_suffix); if(queue.size() > 0) { long duration = 0; for(FeedItem item : queue) { if(item.getMedia() != null) { duration += item.getMedia().getDuration(); } } info += " \u2022 "; info += Converter.getDurationStringLocalized(getActivity(), duration); } infoBar.setText(info); } private QueueRecyclerAdapter.ItemAccess itemAccess = new QueueRecyclerAdapter.ItemAccess() { @Override public int getCount() { return queue != null ? queue.size() : 0; } @Override public FeedItem getItem(int position) { if (queue != null && 0 <= position && position < queue.size()) { return queue.get(position); } return null; } @Override public long getItemDownloadedBytes(FeedItem item) { if (downloaderList != null) { for (Downloader downloader : downloaderList) { if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { Log.d(TAG, "downloaded bytes: " + downloader.getDownloadRequest().getSoFar()); return downloader.getDownloadRequest().getSoFar(); } } } return 0; } @Override public long getItemDownloadSize(FeedItem item) { if (downloaderList != null) { for (Downloader downloader : downloaderList) { if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { Log.d(TAG, "downloaded size: " + downloader.getDownloadRequest().getSize()); return downloader.getDownloadRequest().getSize(); } } } return 0; } @Override public int getItemDownloadProgressPercent(FeedItem item) { if (downloaderList != null) { for (Downloader downloader : downloaderList) { if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { return downloader.getDownloadRequest().getProgressPercent(); } } } return 0; } @Override public LongList getQueueIds() { return queue != null ? LongList.of(FeedItemUtil.getIds(queue)) : new LongList(0); } }; private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { @Override public void update(EventDistributor eventDistributor, Integer arg) { if ((arg & EVENTS) != 0) { Log.d(TAG, "arg: " + arg); loadItems(false); if (isUpdatingFeeds != updateRefreshMenuItemChecker.isRefreshing()) { getActivity().supportInvalidateOptionsMenu(); } } } }; private void loadItems(final boolean restoreScrollPosition) { Log.d(TAG, "loadItems()"); if(subscription != null) { subscription.unsubscribe(); } if (queue == null) { recyclerView.setVisibility(View.GONE); txtvEmpty.setVisibility(View.GONE); progLoading.setVisibility(View.VISIBLE); } subscription = Observable.fromCallable(DBReader::getQueue) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(items -> { if(items != null) { progLoading.setVisibility(View.GONE); queue = items; onFragmentLoaded(restoreScrollPosition); if(recyclerAdapter != null) { recyclerAdapter.notifyDataSetChanged(); } } }, error -> { Log.e(TAG, Log.getStackTraceString(error)); }); } }