diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index f040dc8b4..c7bf4c881 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -574,7 +574,7 @@ public class RouterActivity extends AppCompatActivity { playQueue = new SinglePlayQueue((StreamInfo) info); if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); + NavigationHelper.playOnMainPlayer(this, playQueue, true); } else if (playerChoice.equals(backgroundPlayerKey)) { NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); } else if (playerChoice.equals(popupPlayerKey)) { @@ -587,11 +587,11 @@ public class RouterActivity extends AppCompatActivity { playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); + NavigationHelper.playOnMainPlayer(this, playQueue, true); } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.playOnBackgroundPlayer(this, playQueue); + NavigationHelper.playOnBackgroundPlayer(this, playQueue, true); } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.playOnPopupPlayer(this, playQueue); + NavigationHelper.playOnPopupPlayer(this, playQueue, true); } } }; diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 847153e12..50d723f1f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -50,6 +50,11 @@ public abstract class StreamHistoryDAO implements HistoryDAO> getHistory(); + @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + + " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1") + @Nullable + public abstract StreamHistoryEntity getLatestEntry(final long streamId); + @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") public abstract int deleteStreamHistory(final long streamId); diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index 15940a964..d46d5cd74 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -4,6 +4,9 @@ package org.schabi.newpipe.database.stream.model; import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.ForeignKey; +import android.support.annotation.Nullable; + +import java.util.concurrent.TimeUnit; import static android.arch.persistence.room.ForeignKey.CASCADE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; @@ -22,6 +25,12 @@ public class StreamStateEntity { final public static String JOIN_STREAM_ID = "stream_id"; final public static String STREAM_PROGRESS_TIME = "progress_time"; + + /** Playback state will not be saved, if playback time less than this threshold */ + private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5; + /** Playback state will not be saved, if time left less than this threshold */ + private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10; + @ColumnInfo(name = JOIN_STREAM_ID) private long streamUid; @@ -48,4 +57,18 @@ public class StreamStateEntity { public void setProgressTime(long progressTime) { this.progressTime = progressTime; } + + public boolean isValid(int durationInSeconds) { + final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime); + return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS + && seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof StreamStateEntity) { + return ((StreamStateEntity) obj).streamUid == streamUid + && ((StreamStateEntity) obj).progressTime == progressTime; + } else return false; + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index c89e773f4..30ccfa88f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -89,12 +89,14 @@ import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; import icepick.State; import io.reactivex.Single; @@ -118,7 +120,7 @@ public class VideoDetailFragment private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1; private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2; private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4; - private static final int COMMENTS_UPDATE_FLAG = 0x4; + private static final int COMMENTS_UPDATE_FLAG = 0x8; private boolean autoPlayEnabled; private boolean showRelatedStreams; @@ -136,6 +138,8 @@ public class VideoDetailFragment private Disposable currentWorker; @NonNull private CompositeDisposable disposables = new CompositeDisposable(); + @Nullable + private Disposable positionSubscriber = null; private List sortedVideoStreams; private int selectedVideoStreamIndex = -1; @@ -153,6 +157,7 @@ public class VideoDetailFragment private View thumbnailBackgroundButton; private ImageView thumbnailImageView; private ImageView thumbnailPlayButton; + private AnimatedProgressBar positionView; private View videoTitleRoot; private TextView videoTitleTextView; @@ -165,6 +170,7 @@ public class VideoDetailFragment private TextView detailControlsDownload; private TextView appendControlsDetail; private TextView detailDurationView; + private TextView detailPositionView; private LinearLayout videoDescriptionRootLayout; private TextView videoUploadDateView; @@ -259,6 +265,8 @@ public class VideoDetailFragment // Check if it was loading when the fragment was stopped/paused, if (wasLoading.getAndSet(false)) { selectAndLoadVideo(serviceId, url, name); + } else if (currentInfo != null) { + updateProgressInfo(currentInfo); } } @@ -268,8 +276,10 @@ public class VideoDetailFragment PreferenceManager.getDefaultSharedPreferences(activity) .unregisterOnSharedPreferenceChangeListener(this); + if (positionSubscriber != null) positionSubscriber.dispose(); if (currentWorker != null) currentWorker.dispose(); if (disposables != null) disposables.clear(); + positionSubscriber = null; currentWorker = null; disposables = null; } @@ -462,6 +472,7 @@ public class VideoDetailFragment videoTitleTextView = rootView.findViewById(R.id.detail_video_title_view); videoTitleToggleArrow = rootView.findViewById(R.id.detail_toggle_description_view); videoCountView = rootView.findViewById(R.id.detail_view_count_view); + positionView = rootView.findViewById(R.id.position_view); detailControlsBackground = rootView.findViewById(R.id.detail_controls_background); detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup); @@ -469,6 +480,7 @@ public class VideoDetailFragment detailControlsDownload = rootView.findViewById(R.id.detail_controls_download); appendControlsDetail = rootView.findViewById(R.id.touch_append_detail); detailDurationView = rootView.findViewById(R.id.detail_duration_view); + detailPositionView = rootView.findViewById(R.id.detail_position_view); videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); @@ -536,10 +548,10 @@ public class VideoDetailFragment final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), true); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item), true); break; case 2: if (getFragmentManager() != null) { @@ -890,11 +902,11 @@ public class VideoDetailFragment final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); if (append) { - NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue); + NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue, false); } else { Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); final Intent intent = NavigationHelper.getPlayerIntent( - activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution + activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true ); activity.startService(intent); } @@ -914,9 +926,9 @@ public class VideoDetailFragment private void openNormalBackgroundPlayer(final boolean append) { final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); if (append) { - NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue); + NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue, false); } else { - NavigationHelper.playOnBackgroundPlayer(activity, itemQueue); + NavigationHelper.playOnBackgroundPlayer(activity, itemQueue, true); } } @@ -926,7 +938,7 @@ public class VideoDetailFragment mIntent = NavigationHelper.getPlayerIntent(activity, MainVideoPlayer.class, playQueue, - getSelectedVideoStream().getResolution()); + getSelectedVideoStream().getResolution(), true); startActivity(mIntent); } @@ -1032,6 +1044,8 @@ public class VideoDetailFragment animateView(spinnerToolbar, false, 200); animateView(thumbnailPlayButton, false, 50); animateView(detailDurationView, false, 100); + animateView(detailPositionView, false, 100); + animateView(positionView, false, 50); videoTitleTextView.setText(name != null ? name : ""); videoTitleTextView.setMaxLines(1); @@ -1146,6 +1160,7 @@ public class VideoDetailFragment videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate())); } prepareDescription(info.getDescription()); + updateProgressInfo(info); animateView(spinnerToolbar, true, 500); setupActionBar(info); @@ -1250,4 +1265,37 @@ public class VideoDetailFragment showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); } + + private void updateProgressInfo(@NonNull final StreamInfo info) { + if (positionSubscriber != null) { + positionSubscriber.dispose(); + } + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + final boolean playbackResumeEnabled = + prefs.getBoolean(activity.getString(R.string.enable_watch_history_key), true) + && prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true); + if (!playbackResumeEnabled || info.getDuration() <= 0) { + positionView.setVisibility(View.INVISIBLE); + detailPositionView.setVisibility(View.GONE); + return; + } + final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); + positionSubscriber = recordManager.loadStreamState(info) + .subscribeOn(Schedulers.io()) + .onErrorComplete() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(state -> { + final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()); + positionView.setMax((int) info.getDuration()); + positionView.setProgressAnimated(seconds); + detailPositionView.setText(Localization.getDurationString(seconds)); + animateView(positionView, true, 500); + animateView(detailPositionView, true, 500); + }, e -> { + if (DEBUG) e.printStackTrace(); + }, () -> { + animateView(positionView, false, 500); + animateView(detailPositionView, false, 500); + }); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index dbc3dd8a2..d9c58fbf4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -65,6 +65,12 @@ public abstract class BaseListFragment extends BaseStateFragment implem infoListAdapter = new InfoListAdapter(activity); } + @Override + public void onDetach() { + infoListAdapter.dispose(); + super.onDetach(); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -94,6 +100,8 @@ public abstract class BaseListFragment extends BaseStateFragment implem } updateFlags = 0; } + + itemsList.post(infoListAdapter::updateStates); } /*////////////////////////////////////////////////////////////////////////// @@ -266,13 +274,13 @@ public abstract class BaseListFragment extends BaseStateFragment implem final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { switch (i) { case 0: - NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item), true); break; case 1: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), true); break; case 2: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), true); break; case 3: if (getFragmentManager() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 71865b04d..934e934e9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -170,19 +170,19 @@ public class ChannelFragment extends BaseListInfoFragment { final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), false); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), false); break; case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true); break; case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true); break; case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true); break; case 5: if (getFragmentManager() != null) { @@ -440,11 +440,11 @@ public class ChannelFragment extends BaseListInfoFragment { monitorSubscription(result); headerPlayAllButton.setOnClickListener( - view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener( - view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener( - view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); } private PlayQueue getPlayQueue() { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 2a775fe8f..77aa0a250 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -154,22 +154,22 @@ public class PlaylistFragment extends BaseListInfoFragment { final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), false); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), false); break; case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true); break; case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true); break; case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true); break; case 5: - ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl()); + ShareUtils.shareUrl(requireContext(), item.getName(), item.getUrl()); break; default: break; @@ -301,19 +301,19 @@ public class PlaylistFragment extends BaseListInfoFragment { .subscribe(getPlaylistBookmarkSubscriber()); headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnLongClickListener(view -> { - NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue()); + NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true); return true; }); headerBackgroundButton.setOnLongClickListener(view -> { - NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue()); + NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); return true; }); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index 0e9fd3277..39f7971dd 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -2,12 +2,14 @@ package org.schabi.newpipe.info_list; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import android.view.ViewGroup; import com.nostra13.universalimageloader.core.ImageLoader; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; @@ -59,13 +61,14 @@ public class InfoItemBuilder { this.context = context; } - public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem) { - return buildView(parent, infoItem, false); + public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, @Nullable StreamStateEntity state) { + return buildView(parent, infoItem, state, false); } - public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) { + public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, + @Nullable StreamStateEntity state, boolean useMiniVariant) { InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant); - holder.updateFromItem(infoItem); + holder.updateFromItem(infoItem, state); return holder.itemView; } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 5e7095c7d..7f5b07dbe 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -1,22 +1,25 @@ package org.schabi.newpipe.info_list; import android.app.Activity; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder; import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder; -import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; @@ -50,7 +53,7 @@ import java.util.List; * along with NewPipe. If not, see . */ -public class InfoListAdapter extends RecyclerView.Adapter { +public class InfoListAdapter extends StateObjectsListAdapter { private static final String TAG = InfoListAdapter.class.getSimpleName(); private static final boolean DEBUG = false; @@ -87,6 +90,7 @@ public class InfoListAdapter extends RecyclerView.Adapter(); } @@ -115,50 +119,64 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { + public void addInfoItemList(@Nullable final List data) { if (data != null) { - if (DEBUG) { - Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size()); - } - - int offsetStart = sizeConsideringHeaderOffset(); - infoItemList.addAll(data); - - if (DEBUG) { - Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); - } - - notifyItemRangeInserted(offsetStart, data.size()); - - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeaderOffset(); - notifyItemMoved(offsetStart, footerNow); - - if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow); - } + loadStates(data, infoItemList.size(), () -> addInfoItemListImpl(data)); } } - public void addInfoItem(InfoItem data) { + private void addInfoItemListImpl(@NonNull List data) { + if (DEBUG) { + Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size()); + } + + int offsetStart = sizeConsideringHeaderOffset(); + infoItemList.addAll(data); + + if (DEBUG) { + Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); + } + + notifyItemRangeInserted(offsetStart, data.size()); + + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeaderOffset(); + notifyItemMoved(offsetStart, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow); + } + } + + public void addInfoItem(@Nullable InfoItem data) { if (data != null) { - if (DEBUG) { - Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); - } + loadState(data, infoItemList.size(), () -> addInfoItemImpl(data)); + } + } - int positionInserted = sizeConsideringHeaderOffset(); - infoItemList.add(data); + private void addInfoItemImpl(@NonNull InfoItem data) { + if (DEBUG) { + Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); + } - if (DEBUG) { - Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); - } - notifyItemInserted(positionInserted); + int positionInserted = sizeConsideringHeaderOffset(); + infoItemList.add(data); - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeaderOffset(); - notifyItemMoved(positionInserted, footerNow); + if (DEBUG) { + Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); + } + notifyItemInserted(positionInserted); - if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow); - } + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeaderOffset(); + notifyItemMoved(positionInserted, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow); + } + } + + public void updateStates() { + if (!infoItemList.isEmpty()) { + updateAllStates(infoItemList); } } @@ -167,6 +185,7 @@ public class InfoListAdapter extends RecyclerView.Adapter payloads) { + if (!payloads.isEmpty() && holder instanceof InfoItemHolder) { + for (Object payload : payloads) { + if (payload instanceof StreamStateEntity) { + ((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), + (StreamStateEntity) payload); + } else if (payload instanceof Boolean) { + ((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), + null); + } + } + } else { + onBindViewHolder(holder, position); + } + } + + @Override + protected void onItemStateChanged(int position, @Nullable StreamStateEntity state) { + notifyItemChanged(header == null ? position : position + 1, state != null ? state : false); + } + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) { return new GridLayoutManager.SpanSizeLookup() { @Override diff --git a/app/src/main/java/org/schabi/newpipe/info_list/StateObjectsListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/StateObjectsListAdapter.java new file mode 100644 index 000000000..23ae17bac --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/StateObjectsListAdapter.java @@ -0,0 +1,189 @@ +package org.schabi.newpipe.info_list; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.util.SparseArray; + +import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.util.SparseArrayUtils; + +import java.util.List; +import java.util.Objects; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + +public abstract class StateObjectsListAdapter extends RecyclerView.Adapter { + + private final SparseArray states; + private final HistoryRecordManager recordManager; + private final CompositeDisposable stateLoaders; + private final Context context; + + public StateObjectsListAdapter(Context context) { + this.states = new SparseArray<>(); + this.recordManager = new HistoryRecordManager(context); + this.context = context; + this.stateLoaders = new CompositeDisposable(); + } + + @Nullable + public StreamStateEntity getState(int position) { + return states.get(position); + } + + protected void clearStates() { + states.clear(); + } + + private void appendStates(List statesEntities, int offset) { + for (int i = 0; i < statesEntities.size(); i++) { + final StreamStateEntity state = statesEntities.get(i); + if (state != null) { + states.append(offset + i, state); + } + } + } + + private void appendState(StreamStateEntity statesEntity, int offset) { + if (statesEntity != null) { + states.append(offset, statesEntity); + } + } + + protected void removeState(int index) { + states.remove(index); + } + + protected void moveState(int from, int to) { + final StreamStateEntity item = states.get(from); + if (from < to) { + SparseArrayUtils.shiftItemsDown(states, from, to); + } else { + SparseArrayUtils.shiftItemsUp(states, to, from); + } + states.put(to, item); + } + + protected void loadStates(List list, int offset, Runnable callback) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + appendStates(streamStateEntities, offset); + callback.run(); + }, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + callback.run(); + }) + ); + } else { + callback.run(); + } + } + + protected void loadState(InfoItem item, int offset, Runnable callback) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadStreamState(item) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + appendState(streamStateEntities[0], offset); + callback.run(); + }, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + callback.run(); + }) + ); + } else { + callback.run(); + } + } + + protected void loadStatesForLocal(List list, int offset, Runnable callback) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadLocalStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + appendStates(streamStateEntities, offset); + callback.run(); + }, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + callback.run(); + }) + ); + } else { + callback.run(); + } + } + + private void processStatesUpdates(List streamStateEntities) { + for (int i = 0; i < streamStateEntities.size(); i++) { + final StreamStateEntity newState = streamStateEntities.get(i); + if (!Objects.equals(states.get(i), newState)) { + if (newState == null) { + states.remove(i); + } else { + states.put(i, newState); + } + onItemStateChanged(i, newState); + } + } + } + + protected void updateAllStates(List list) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::processStatesUpdates, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + }) + ); + } else { + final int[] positions = SparseArrayUtils.getKeys(states); + states.clear(); + for (int pos : positions) onItemStateChanged(pos, null); + } + } + + protected void updateAllLocalStates(List list) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadLocalStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::processStatesUpdates, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + }) + ); + } else { + final int[] positions = SparseArrayUtils.getKeys(states); + states.clear(); + for (int pos : positions) onItemStateChanged(pos, null); + } + } + + public void dispose() { + stateLoaders.dispose(); + } + + protected boolean isPlaybackStatesVisible() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) + && prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true) + && prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true); + } + + protected abstract void onItemStateChanged(int position, @Nullable StreamStateEntity state); + +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java index 1e5c726a4..317934455 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.info_list.holder; +import android.support.annotation.Nullable; import android.view.ViewGroup; import android.widget.TextView; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; @@ -38,8 +40,8 @@ public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder { } @Override - public void updateFromItem(final InfoItem infoItem) { - super.updateFromItem(infoItem); + public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) { + super.updateFromItem(infoItem, state); if (!(infoItem instanceof ChannelInfoItem)) return; final ChannelInfoItem item = (ChannelInfoItem) infoItem; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java index ca783833a..a191707c0 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.info_list.holder; +import android.support.annotation.Nullable; import android.view.ViewGroup; import android.widget.TextView; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; @@ -30,7 +32,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder { } @Override - public void updateFromItem(final InfoItem infoItem) { + public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) { if (!(infoItem instanceof ChannelInfoItem)) return; final ChannelInfoItem item = (ChannelInfoItem) infoItem; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java index 46e4b4563..4ecf86961 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java @@ -1,14 +1,14 @@ package org.schabi.newpipe.info_list.holder; +import android.support.annotation.Nullable; import android.view.ViewGroup; import android.widget.TextView; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.util.Localization; /* * Created by Christian Schabesberger on 12.02.17. @@ -41,8 +41,8 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder { } @Override - public void updateFromItem(final InfoItem infoItem) { - super.updateFromItem(infoItem); + public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) { + super.updateFromItem(infoItem, state); if (!(infoItem instanceof CommentsInfoItem)) return; final CommentsInfoItem item = (CommentsInfoItem) infoItem; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index ce8412b20..3d3a9bb09 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.info_list.holder; +import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.text.util.Linkify; import android.view.View; @@ -8,6 +9,7 @@ import android.widget.TextView; import org.jsoup.helper.StringUtil; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; @@ -65,7 +67,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { } @Override - public void updateFromItem(final InfoItem infoItem) { + public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) { if (!(infoItem instanceof CommentsInfoItem)) return; final CommentsInfoItem item = (CommentsInfoItem) infoItem; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java index ebb5b4114..3bc0d9e54 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.info_list.holder; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.ViewGroup; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; @@ -35,5 +37,8 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder { this.itemBuilder = infoItemBuilder; } - public abstract void updateFromItem(final InfoItem infoItem); + public abstract void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state); + + public void updateState(final InfoItem infoItem, @Nullable final StreamStateEntity state) { + } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java index b6bd2f389..f9d617e66 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.info_list.holder; +import android.support.annotation.Nullable; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; @@ -30,7 +32,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder { } @Override - public void updateFromItem(final InfoItem infoItem) { + public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) { if (!(infoItem instanceof PlaylistInfoItem)) return; final PlaylistInfoItem item = (PlaylistInfoItem) infoItem; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java index 0a7705427..25502bc81 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.info_list.holder; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.view.ViewGroup; import android.widget.TextView; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; @@ -40,8 +42,8 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder { } @Override - public void updateFromItem(final InfoItem infoItem) { - super.updateFromItem(infoItem); + public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) { + super.updateFromItem(infoItem, state); if (!(infoItem instanceof StreamInfoItem)) return; final StreamInfoItem item = (StreamInfoItem) infoItem; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 048b907af..aa2a3f878 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.info_list.holder; +import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.view.View; import android.view.ViewGroup; @@ -7,12 +8,17 @@ import android.widget.ImageView; import android.widget.TextView; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.views.AnimatedProgressBar; + +import java.util.concurrent.TimeUnit; public class StreamMiniInfoItemHolder extends InfoItemHolder { @@ -20,6 +26,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { public final TextView itemVideoTitleView; public final TextView itemUploaderView; public final TextView itemDurationView; + public final AnimatedProgressBar itemProgressView; StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); @@ -28,6 +35,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); itemUploaderView = itemView.findViewById(R.id.itemUploaderView); itemDurationView = itemView.findViewById(R.id.itemDurationView); + itemProgressView = itemView.findViewById(R.id.itemProgressView); } public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { @@ -35,7 +43,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { } @Override - public void updateFromItem(final InfoItem infoItem) { + public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) { if (!(infoItem instanceof StreamInfoItem)) return; final StreamInfoItem item = (StreamInfoItem) infoItem; @@ -47,13 +55,22 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); + if (state != null) { + itemProgressView.setVisibility(View.VISIBLE); + itemProgressView.setMax((int) item.getDuration()); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setVisibility(View.GONE); + } } else if (item.getStreamType() == StreamType.LIVE_STREAM) { itemDurationView.setText(R.string.duration_live); itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); + itemProgressView.setVisibility(View.GONE); } else { itemDurationView.setVisibility(View.GONE); + itemProgressView.setVisibility(View.GONE); } // Default thumbnail is shown on error, while loading and if the url is empty @@ -83,6 +100,22 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { } } + @Override + public void updateState(final InfoItem infoItem, @Nullable final StreamStateEntity state) { + final StreamInfoItem item = (StreamInfoItem) infoItem; + if (state != null && item.getDuration() > 0 && item.getStreamType() != StreamType.LIVE_STREAM) { + itemProgressView.setMax((int) item.getDuration()); + if (itemProgressView.getVisibility() == View.VISIBLE) { + itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + AnimationUtils.animateView(itemProgressView, true, 500); + } + } else if (itemProgressView.getVisibility() == View.VISIBLE) { + AnimationUtils.animateView(itemProgressView, false, 500); + } + } + private void enableLongClick(final StreamInfoItem item) { itemView.setLongClickable(true); itemView.setOnLongClickListener(view -> { diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index abdf82353..94672bd49 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -76,6 +76,8 @@ public abstract class BaseLocalListFragment extends BaseStateFragment } updateFlags = 0; } + + itemsList.post(itemListAdapter::updateStates); } /*////////////////////////////////////////////////////////////////////////// @@ -150,6 +152,7 @@ public abstract class BaseLocalListFragment extends BaseStateFragment public void onDestroyView() { super.onDestroyView(); itemsList = null; + itemListAdapter.dispose(); itemListAdapter = null; } diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index e298dedd3..d29e85ee3 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.local; import android.app.Activity; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -8,6 +10,8 @@ import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; +import org.schabi.newpipe.info_list.StateObjectsListAdapter; import org.schabi.newpipe.local.holder.LocalItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder; @@ -45,7 +49,7 @@ import java.util.List; * along with NewPipe. If not, see . */ -public class LocalItemListAdapter extends RecyclerView.Adapter { +public class LocalItemListAdapter extends StateObjectsListAdapter { private static final String TAG = LocalItemListAdapter.class.getSimpleName(); private static final boolean DEBUG = false; @@ -72,6 +76,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter(); dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, @@ -86,39 +91,49 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { + public void addItems(@Nullable List data) { if (data != null) { - if (DEBUG) { - Log.d(TAG, "addItems() before > localItems.size() = " + - localItems.size() + ", data.size() = " + data.size()); - } + loadStatesForLocal(data, localItems.size(), () -> addItemsImpl(data)); + } + } - int offsetStart = sizeConsideringHeader(); - localItems.addAll(data); + private void addItemsImpl(@NonNull List data) { + if (DEBUG) { + Log.d(TAG, "addItems() before > localItems.size() = " + + localItems.size() + ", data.size() = " + data.size()); + } - if (DEBUG) { - Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + - ", localItems.size() = " + localItems.size() + - ", header = " + header + ", footer = " + footer + - ", showFooter = " + showFooter); - } + int offsetStart = sizeConsideringHeader(); + localItems.addAll(data); - notifyItemRangeInserted(offsetStart, data.size()); + if (DEBUG) { + Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + + ", localItems.size() = " + localItems.size() + + ", header = " + header + ", footer = " + footer + + ", showFooter = " + showFooter); + } - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeader(); - notifyItemMoved(offsetStart, footerNow); + notifyItemRangeInserted(offsetStart, data.size()); - if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart + - " to " + footerNow); - } + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeader(); + notifyItemMoved(offsetStart, footerNow); + + if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart + + " to " + footerNow); + } + } + + public void updateStates() { + if (!localItems.isEmpty()) { + updateAllLocalStates(localItems); } } public void removeItem(final LocalItem data) { final int index = localItems.indexOf(data); - localItems.remove(index); + removeState(index); notifyItemRemoved(index + (header != null ? 1 : 0)); } @@ -130,6 +145,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter= localItems.size() || actualTo >= localItems.size()) return false; localItems.add(actualTo, localItems.remove(actualFrom)); + moveState(actualFrom, actualTo); notifyItemMoved(fromAdapterPosition, toAdapterPosition); return true; } @@ -139,6 +155,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter payloads) { + if (!payloads.isEmpty() && holder instanceof LocalItemHolder) { + for (Object payload : payloads) { + if (payload instanceof StreamStateEntity) { + ((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), + (StreamStateEntity) payload); + } else if (payload instanceof Boolean) { + ((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), + null); + } + } + } else { + onBindViewHolder(holder, position); + } + } + + @Override + protected void onItemStateChanged(int position, @Nullable StreamStateEntity state) { + notifyItemChanged(header == null ? position : position + 1, state != null ? state : false); + } + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) { return new GridLayoutManager.SpanSizeLookup() { @Override diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java index e422b332a..5973ad920 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java @@ -112,7 +112,10 @@ public final class PlaylistAppendDialog extends PlaylistDialog { public void onDestroyView() { super.onDestroyView(); if (playlistReactor != null) playlistReactor.dispose(); - if (playlistAdapter != null) playlistAdapter.unsetSelectedListener(); + if (playlistAdapter != null) { + playlistAdapter.dispose(); + playlistAdapter.unsetSelectedListener(); + } playlistReactor = null; playlistRecyclerView = null; diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 56453773a..dd2d67e43 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -26,23 +26,29 @@ import android.support.annotation.NonNull; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.dao.StreamDAO; import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; +import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; +import io.reactivex.Completable; import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.Single; @@ -80,9 +86,9 @@ public class HistoryRecordManager { final Date currentTime = new Date(); return Maybe.fromCallable(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); - StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(); + StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId); - if (latestEntry != null && latestEntry.getStreamUid() == streamId) { + if (latestEntry != null) { streamHistoryTable.delete(latestEntry); latestEntry.setAccessDate(currentTime); latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1); @@ -99,7 +105,7 @@ public class HistoryRecordManager { } public Single deleteWholeStreamHistory() { - return Single.fromCallable(() -> streamHistoryTable.deleteAll()) + return Single.fromCallable(streamHistoryTable::deleteAll) .subscribeOn(Schedulers.io()); } @@ -160,7 +166,7 @@ public class HistoryRecordManager { } public Single deleteWholeSearchHistory() { - return Single.fromCallable(() -> searchHistoryTable.deleteAll()) + return Single.fromCallable(searchHistoryTable::deleteAll) .subscribeOn(Schedulers.io()); } @@ -180,21 +186,104 @@ public class HistoryRecordManager { // Stream State History /////////////////////////////////////////////////////// - @SuppressWarnings("unused") - public Maybe loadStreamState(final StreamInfo info) { - return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info))) - .flatMap(streamId -> streamStateTable.getState(streamId).firstElement()) - .flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0))) + public Maybe getStreamHistory(final StreamInfo info) { + return Maybe.fromCallable(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return streamHistoryTable.getLatestEntry(streamId); + }).subscribeOn(Schedulers.io()); + } + + public Maybe loadStreamState(final PlayQueueItem queueItem) { + return queueItem.getStream() + .map((info) -> streamTable.upsert(new StreamEntity(info))) + .flatMapPublisher(streamStateTable::getState) + .firstElement() + .flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0))) + .filter(state -> state.isValid((int) queueItem.getDuration())) .subscribeOn(Schedulers.io()); } - public Maybe saveStreamState(@NonNull final StreamInfo info, final long progressTime) { - return Maybe.fromCallable(() -> database.runInTransaction(() -> { + public Maybe loadStreamState(final StreamInfo info) { + return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info))) + .flatMapPublisher(streamStateTable::getState) + .firstElement() + .flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0))) + .filter(state -> state.isValid((int) info.getDuration())) + .subscribeOn(Schedulers.io()); + } + + public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) { + return Completable.fromAction(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); - return streamStateTable.upsert(new StreamStateEntity(streamId, progressTime)); + final StreamStateEntity state = new StreamStateEntity(streamId, progressTime); + if (state.isValid((int) info.getDuration())) { + streamStateTable.upsert(state); + } else { + streamStateTable.deleteState(streamId); + } })).subscribeOn(Schedulers.io()); } + public Single loadStreamState(final InfoItem info) { + return Single.fromCallable(() -> { + final List entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst(); + if (entities.isEmpty()) { + return new StreamStateEntity[]{null}; + } + final List states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst(); + if (states.isEmpty()) { + return new StreamStateEntity[]{null}; + } + return new StreamStateEntity[]{states.get(0)}; + }).subscribeOn(Schedulers.io()); + } + + public Single> loadStreamStateBatch(final List infos) { + return Single.fromCallable(() -> { + final List result = new ArrayList<>(infos.size()); + for (InfoItem info : infos) { + final List entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst(); + if (entities.isEmpty()) { + result.add(null); + continue; + } + final List states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst(); + if (states.isEmpty()) { + result.add(null); + continue; + } + result.add(states.get(0)); + } + return result; + }).subscribeOn(Schedulers.io()); + } + + public Single> loadLocalStreamStateBatch(final List items) { + return Single.fromCallable(() -> { + final List result = new ArrayList<>(items.size()); + for (LocalItem item : items) { + long streamId; + if (item instanceof StreamStatisticsEntry) { + streamId = ((StreamStatisticsEntry) item).streamId; + } else if (item instanceof PlaylistStreamEntity) { + streamId = ((PlaylistStreamEntity) item).getStreamUid(); + } else if (item instanceof PlaylistStreamEntry) { + streamId = ((PlaylistStreamEntry) item).streamId; + } else { + result.add(null); + continue; + } + final List states = streamStateTable.getState(streamId).blockingFirst(); + if (states.isEmpty()) { + result.add(null); + continue; + } + result.add(states.get(0)); + } + return result; + }).subscribeOn(Schedulers.io()); + } + /////////////////////////////////////////////////////// // Utility /////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 5a62a3969..b97fdf8f2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -310,11 +310,11 @@ public class StatisticsPlaylistFragment } headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); sortButton.setOnClickListener(view -> toggleSortMode()); hideLoading(); @@ -377,19 +377,19 @@ public class StatisticsPlaylistFragment final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem), false); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem), false); break; case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true); break; case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true); break; case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true); break; case 5: deleteEntry(index); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java index 889751afa..c00fa1fb4 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.local.LocalItemBuilder; import java.text.DateFormat; @@ -38,5 +40,8 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder { this.itemBuilder = itemBuilder; } - public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); + public abstract void updateFromItem(final LocalItem item, @Nullable final StreamStateEntity state, final DateFormat dateFormat); + + public void updateState(final LocalItem localItem, @Nullable final StreamStateEntity state) { + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java index 8743684ee..0e6eca9ba 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.util.ImageDisplayConstants; @@ -21,7 +23,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { } @Override - public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) { if (!(localItem instanceof PlaylistMetadataEntry)) return; final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; @@ -32,6 +34,6 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); - super.updateFromItem(localItem, dateFormat); + super.updateFromItem(localItem, state, dateFormat); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index e591b73e5..0c4e66c9d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.view.MotionEvent; import android.view.View; @@ -10,12 +11,16 @@ import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.text.DateFormat; +import java.util.concurrent.TimeUnit; public class LocalPlaylistStreamItemHolder extends LocalItemHolder { @@ -24,6 +29,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { public final TextView itemAdditionalDetailsView; public final TextView itemDurationView; public final View itemHandleView; + public final AnimatedProgressBar itemProgressView; LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); @@ -33,6 +39,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails); itemDurationView = itemView.findViewById(R.id.itemDurationView); itemHandleView = itemView.findViewById(R.id.itemHandle); + itemProgressView = itemView.findViewById(R.id.itemProgressView); } public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { @@ -40,7 +47,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { } @Override - public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) { if (!(localItem instanceof PlaylistStreamEntry)) return; final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; @@ -53,6 +60,13 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); + if (state != null) { + itemProgressView.setVisibility(View.VISIBLE); + itemProgressView.setMax((int) item.duration); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setVisibility(View.GONE); + } } else { itemDurationView.setVisibility(View.GONE); } @@ -79,6 +93,23 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { itemHandleView.setOnTouchListener(getOnTouchListener(item)); } + @Override + public void updateState(LocalItem localItem, @Nullable StreamStateEntity state) { + if (!(localItem instanceof PlaylistStreamEntry)) return; + final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; + if (state != null && item.duration > 0) { + itemProgressView.setMax((int) item.duration); + if (itemProgressView.getVisibility() == View.VISIBLE) { + itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + AnimationUtils.animateView(itemProgressView, true, 500); + } + } else if (itemProgressView.getVisibility() == View.VISIBLE) { + AnimationUtils.animateView(itemProgressView, false, 500); + } + } + private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) { return (view, motionEvent) -> { view.performClick(); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index 57a5794e3..b24051a4f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -10,12 +10,16 @@ import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.text.DateFormat; +import java.util.concurrent.TimeUnit; /* * Created by Christian Schabesberger on 01.08.16. @@ -45,6 +49,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { public final TextView itemDurationView; @Nullable public final TextView itemAdditionalDetails; + public final AnimatedProgressBar itemProgressView; public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) { this(itemBuilder, R.layout.list_stream_item, parent); @@ -58,6 +63,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemUploaderView = itemView.findViewById(R.id.itemUploaderView); itemDurationView = itemView.findViewById(R.id.itemDurationView); itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); + itemProgressView = itemView.findViewById(R.id.itemProgressView); } private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, @@ -70,7 +76,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { } @Override - public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) { if (!(localItem instanceof StreamStatisticsEntry)) return; final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; @@ -82,8 +88,16 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); + if (state != null) { + itemProgressView.setVisibility(View.VISIBLE); + itemProgressView.setMax((int) item.duration); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setVisibility(View.GONE); + } } else { itemDurationView.setVisibility(View.GONE); + itemProgressView.setVisibility(View.GONE); } if (itemAdditionalDetails != null) { @@ -108,4 +122,21 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { return true; }); } + + @Override + public void updateState(LocalItem localItem, @Nullable StreamStateEntity state) { + if (!(localItem instanceof StreamStatisticsEntry)) return; + final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; + if (state != null && item.duration > 0) { + itemProgressView.setMax((int) item.duration); + if (itemProgressView.getVisibility() == View.VISIBLE) { + itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + AnimationUtils.animateView(itemProgressView, true, 500); + } + } else if (itemProgressView.getVisibility() == View.VISIBLE) { + AnimationUtils.animateView(itemProgressView, false, 500); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java index 5d6f192e1..2a81f9571 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java @@ -1,11 +1,13 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.local.LocalItemBuilder; import java.text.DateFormat; @@ -31,7 +33,7 @@ public abstract class PlaylistItemHolder extends LocalItemHolder { } @Override - public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) { itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { itemBuilder.getOnItemSelectedListener().selected(localItem); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index 5b2a88d38..bdcd42f67 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.util.ImageDisplayConstants; @@ -21,7 +23,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { } @Override - public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) { if (!(localItem instanceof PlaylistRemoteEntity)) return; final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; @@ -33,6 +35,6 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); - super.updateFromItem(localItem, dateFormat); + super.updateFromItem(localItem, state, dateFormat); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index dc101fade..501016642 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -319,11 +319,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); hideLoading(); } @@ -534,20 +534,20 @@ public class LocalPlaylistFragment extends BaseLocalListFragment initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, + /*playOnInit=*/true)) + .subscribe( + state -> queue.setRecovery(queue.getIndex(), state.getProgressTime()), + error -> { + if (DEBUG) error.printStackTrace(); + } + ); + databaseUpdateReactor.add(stateLoader); + return; + } } - // Good to go... initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, /*playOnInit=*/true); @@ -615,6 +634,9 @@ public abstract class BasePlayer implements break; case Player.STATE_ENDED: // 4 changeState(STATE_COMPLETED); + if (currentMetadata != null) { + resetPlaybackState(currentMetadata.getMetadata()); + } isPrepared = false; break; } @@ -721,6 +743,7 @@ public abstract class BasePlayer implements case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_INTERNAL: if (playQueue.getIndex() != newWindowIndex) { + resetPlaybackState(playQueue.getItem()); playQueue.setIndex(newWindowIndex); } break; @@ -750,6 +773,9 @@ public abstract class BasePlayer implements @Override public void onSeekProcessed() { if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); + if (isPrepared) { + savePlaybackState(); + } } /*////////////////////////////////////////////////////////////////////////// // Playback Listener @@ -1017,27 +1043,40 @@ public abstract class BasePlayer implements } } - protected void savePlaybackState(final StreamInfo info, final long progress) { + private void savePlaybackState(final StreamInfo info, final long progress) { if (info == null) return; + if (DEBUG) Log.d(TAG, "savePlaybackState() called"); final Disposable stateSaver = recordManager.saveStreamState(info, progress) .observeOn(AndroidSchedulers.mainThread()) + .doOnError((e) -> { + if (DEBUG) e.printStackTrace(); + }) .onErrorComplete() - .subscribe( - ignored -> {/* successful */}, - error -> Log.e(TAG, "savePlaybackState() failure: ", error) - ); + .subscribe(); databaseUpdateReactor.add(stateSaver); } - private void savePlaybackState() { + private void resetPlaybackState(final PlayQueueItem queueItem) { + if (queueItem == null) return; + final Disposable stateSaver = queueItem.getStream() + .flatMapCompletable(info -> recordManager.saveStreamState(info, 0)) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError((e) -> { + if (DEBUG) e.printStackTrace(); + }) + .onErrorComplete() + .subscribe(); + databaseUpdateReactor.add(stateSaver); + } + + public void resetPlaybackState(final StreamInfo info) { + savePlaybackState(info, 0); + } + + public void savePlaybackState() { if (simpleExoPlayer == null || currentMetadata == null) return; final StreamInfo currentInfo = currentMetadata.getMetadata(); - - if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS && - simpleExoPlayer.getCurrentPosition() < - simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) { - savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); - } + savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); } private void maybeUpdateCurrentMetadata() { @@ -1225,4 +1264,10 @@ public abstract class BasePlayer implements public boolean gotDestroyed() { return simpleExoPlayer == null; } + + private boolean isPlaybackResumeEnabled() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) + && prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 0bb9c7b2b..f48ad7019 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -248,6 +248,12 @@ public final class MainVideoPlayer extends AppCompatActivity super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase)); } + @Override + protected void onPause() { + playerImpl.savePlaybackState(); + super.onPause(); + } + /*////////////////////////////////////////////////////////////////////////// // State Saving //////////////////////////////////////////////////////////////////////////*/ @@ -583,7 +589,8 @@ public final class MainVideoPlayer extends AppCompatActivity this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), - this.getPlaybackQuality() + this.getPlaybackQuality(), + false ); context.startService(intent); @@ -605,7 +612,8 @@ public final class MainVideoPlayer extends AppCompatActivity this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), - this.getPlaybackQuality() + this.getPlaybackQuality(), + false ); context.startService(intent); diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 7578c444c..3782d85c0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -325,6 +325,7 @@ public final class PopupVideoPlayer extends Service { isPopupClosing = true; if (playerImpl != null) { + playerImpl.savePlaybackState(); if (playerImpl.getRootView() != null) { windowManager.removeView(playerImpl.getRootView()); } @@ -565,7 +566,8 @@ public final class PopupVideoPlayer extends Service { this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), - this.getPlaybackQuality() + this.getPlaybackQuality(), + false ); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 3e04f1e3a..bdd31f21b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -188,7 +188,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(), this.player.getPlaybackSkipSilence(), - null + null, + false ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index d15dc8b28..7ce778e6a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -542,6 +542,11 @@ public abstract class VideoPlayer extends BasePlayer playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed())); super.onPrepared(playWhenReady); + + if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) { + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION); + } } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java index e1ecc662d..ac79fee23 100644 --- a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java +++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java @@ -124,7 +124,7 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { .observeOn(AndroidSchedulers.mainThread()) .subscribe(info -> { PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000); - NavigationHelper.playOnPopupPlayer(context, playQueue); + NavigationHelper.playOnPopupPlayer(context, playQueue, false); }); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 98ae3a88a..89c4b33fe 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -69,12 +69,14 @@ public class NavigationHelper { public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, - @Nullable final String quality) { + @Nullable final String quality, + final boolean resumePlayback) { Intent intent = new Intent(context, targetClazz); final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); if (cacheKey != null) intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); + intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback); return intent; } @@ -82,16 +84,18 @@ public class NavigationHelper { @NonNull public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, - @NonNull final PlayQueue playQueue) { - return getPlayerIntent(context, targetClazz, playQueue, null); + @NonNull final PlayQueue playQueue, + final boolean resumePlayback) { + return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback); } @NonNull public static Intent getPlayerEnqueueIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, - final boolean selectOnAppend) { - return getPlayerIntent(context, targetClazz, playQueue) + final boolean selectOnAppend, + final boolean resumePlayback) { + return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) .putExtra(BasePlayer.APPEND_ONLY, true) .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); } @@ -104,40 +108,41 @@ public class NavigationHelper { final float playbackSpeed, final float playbackPitch, final boolean playbackSkipSilence, - @Nullable final String playbackQuality) { - return getPlayerIntent(context, targetClazz, playQueue, playbackQuality) + @Nullable final String playbackQuality, + final boolean resumePlayback) { + return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch) .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence); } - public static void playOnMainPlayer(final Context context, final PlayQueue queue) { - final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue); + public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback); playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(playerIntent); } - public static void playOnPopupPlayer(final Context context, final PlayQueue queue) { + public static void playOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { if (!PermissionHelper.isPopupEnabled(context)) { PermissionHelper.showPopupEnablementToast(context); return; } Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue)); + startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback)); } - public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue) { + public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); - startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue)); + startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback)); } - public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue) { - enqueueOnPopupPlayer(context, queue, false); + public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + enqueueOnPopupPlayer(context, queue, false, resumePlayback); } - public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) { + public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) { if (!PermissionHelper.isPopupEnabled(context)) { PermissionHelper.showPopupEnablementToast(context); return; @@ -145,17 +150,17 @@ public class NavigationHelper { Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); startService(context, - getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend)); + getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend, resumePlayback)); } - public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue) { - enqueueOnBackgroundPlayer(context, queue, false); + public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + enqueueOnBackgroundPlayer(context, queue, false, resumePlayback); } - public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) { + public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); startService(context, - getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend)); + getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend, resumePlayback)); } public static void startService(@NonNull final Context context, @NonNull final Intent intent) { diff --git a/app/src/main/java/org/schabi/newpipe/util/SparseArrayUtils.java b/app/src/main/java/org/schabi/newpipe/util/SparseArrayUtils.java new file mode 100644 index 000000000..d17c9aa42 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/SparseArrayUtils.java @@ -0,0 +1,30 @@ +package org.schabi.newpipe.util; + +import android.util.SparseArray; + +public abstract class SparseArrayUtils { + + public static void shiftItemsDown(SparseArray sparseArray, int lower, int upper) { + for (int i = lower + 1; i <= upper; i++) { + final T o = sparseArray.get(i); + sparseArray.put(i - 1, o); + sparseArray.remove(i); + } + } + + public static void shiftItemsUp(SparseArray sparseArray, int lower, int upper) { + for (int i = upper - 1; i >= lower; i--) { + final T o = sparseArray.get(i); + sparseArray.put(i + 1, o); + sparseArray.remove(i); + } + } + + public static int[] getKeys(SparseArray sparseArray) { + final int[] result = new int[sparseArray.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = sparseArray.keyAt(i); + } + return result; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java b/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java new file mode 100644 index 000000000..fe3e0d7bc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java @@ -0,0 +1,64 @@ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.widget.ProgressBar; + +public final class AnimatedProgressBar extends ProgressBar { + + @Nullable + private ProgressBarAnimation animation = null; + + public AnimatedProgressBar(Context context) { + super(context); + } + + public AnimatedProgressBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AnimatedProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public synchronized void setProgressAnimated(int progress) { + cancelAnimation(); + animation = new ProgressBarAnimation(this, getProgress(), progress); + startAnimation(animation); + } + + private void cancelAnimation() { + if (animation != null) { + animation.cancel(); + animation = null; + } + clearAnimation(); + } + + private static class ProgressBarAnimation extends Animation { + + private final AnimatedProgressBar progressBar; + private final float from; + private final float to; + + ProgressBarAnimation(AnimatedProgressBar progressBar, float from, float to) { + super(); + this.progressBar = progressBar; + this.from = from; + this.to = to; + setDuration(500); + setInterpolator(new AccelerateDecelerateInterpolator()); + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + super.applyTransformation(interpolatedTime, t); + float value = from + (to - from) * interpolatedTime; + progressBar.setProgress((int) value); + } + } +} diff --git a/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml b/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml new file mode 100644 index 000000000..1ec9f67b6 --- /dev/null +++ b/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml b/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml new file mode 100644 index 000000000..c326c5c04 --- /dev/null +++ b/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml b/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml new file mode 100644 index 000000000..404410f98 --- /dev/null +++ b/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_youtube_horizontal_light.xml b/app/src/main/res/drawable/progress_youtube_horizontal_light.xml new file mode 100644 index 000000000..120a6e5fb --- /dev/null +++ b/app/src/main/res/drawable/progress_youtube_horizontal_light.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 8cdc2f307..15d6b7a17 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -1,12 +1,14 @@ + tools:ignore="RtlHardcoded" + android:orientation="horizontal" + android:baselineAligned="false"> @@ -67,10 +70,10 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:background="#64000000" - android:paddingBottom="10dp" android:paddingLeft="30dp" - android:paddingRight="30dp" android:paddingTop="10dp" + android:paddingRight="30dp" + android:paddingBottom="10dp" android:text="@string/hold_to_append" android:textColor="@android:color/white" android:textSize="20sp" @@ -84,17 +87,17 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|right" - android:layout_marginBottom="8dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="8dp" + android:layout_marginRight="12dp" + android:layout_marginBottom="8dp" android:alpha=".6" android:background="#23000000" android:gravity="center" - android:paddingBottom="2dp" android:paddingLeft="6dp" - android:paddingRight="6dp" android:paddingTop="2dp" + android:paddingRight="6dp" + android:paddingBottom="2dp" android:textAllCaps="true" android:textColor="@android:color/white" android:textSize="12sp" @@ -103,10 +106,48 @@ tools:ignore="RtlHardcoded" tools:text="12:38" tools:visibility="visible" /> + + + + + tools:ignore="ContentDescription" /> @@ -179,9 +219,9 @@ android:id="@+id/detail_content_root_hiding" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingBottom="10dp" android:layout_below="@+id/detail_title_root_layout" android:orientation="vertical" + android:paddingBottom="10dp" android:visibility="gone" tools:visibility="visible"> @@ -191,8 +231,8 @@ android:layout_width="match_parent" android:layout_height="55dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="6dp" + android:layout_marginRight="12dp" android:baselineAligned="false" android:orientation="horizontal"> @@ -201,8 +241,8 @@ android:id="@+id/detail_uploader_root_layout" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_toLeftOf="@id/details_panel" android:layout_toStartOf="@id/details_panel" + android:layout_toLeftOf="@id/details_panel" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" android:orientation="horizontal" @@ -213,8 +253,7 @@ android:layout_width="@dimen/video_item_detail_uploader_image_size" android:layout_height="@dimen/video_item_detail_uploader_image_size" android:contentDescription="@string/detail_uploader_thumbnail_view_description" - android:src="@drawable/buddy" - tools:ignore="RtlHardcoded" /> + android:src="@drawable/buddy" /> @@ -371,8 +410,8 @@ android:drawableTop="?attr/audio" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_background_title" android:textSize="12sp" /> @@ -388,8 +427,8 @@ android:drawableTop="?attr/popup" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_popup_title" android:textSize="12sp" /> @@ -405,8 +444,8 @@ android:drawableTop="?attr/download" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/download" android:textSize="12sp" /> @@ -444,10 +483,10 @@ android:id="@+id/detail_description_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="8dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="3dp" + android:layout_marginRight="12dp" + android:layout_marginBottom="8dp" android:textAppearance="?android:attr/textAppearanceMedium" android:textIsSelectable="true" android:textSize="@dimen/video_item_detail_description_text_size" @@ -490,7 +529,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/video_item_detail" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusableInTouchMode="true"> - + - + - + - - + + - + - + - + - - + - + - - + - - + - + - + + - + + - - + - - + - - + - - + + - - + + - + + - + + - - + + - - + - + - + + - + + - + - + - - - + - + - - + - + + + - + - + + - + - + - - + - + - + - + + - + - - + - + + + + + + + + - + - + - + - + diff --git a/app/src/main/res/layout/list_stream_grid_item.xml b/app/src/main/res/layout/list_stream_grid_item.xml index cf73bf9b1..fc9235e8d 100644 --- a/app/src/main/res/layout/list_stream_grid_item.xml +++ b/app/src/main/res/layout/list_stream_grid_item.xml @@ -66,4 +66,16 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textSize="@dimen/video_item_search_uploader_text_size" tools:text="Uploader"/> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_stream_item.xml b/app/src/main/res/layout/list_stream_item.xml index db4af7f8c..02e8f1531 100644 --- a/app/src/main/res/layout/list_stream_item.xml +++ b/app/src/main/res/layout/list_stream_item.xml @@ -79,4 +79,16 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textSize="@dimen/video_item_search_upload_date_text_size" tools:text="2 years ago • 10M views"/> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_stream_mini_item.xml b/app/src/main/res/layout/list_stream_mini_item.xml index bffd13a6f..667798128 100644 --- a/app/src/main/res/layout/list_stream_mini_item.xml +++ b/app/src/main/res/layout/list_stream_mini_item.xml @@ -69,4 +69,16 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textSize="@dimen/video_item_search_uploader_text_size" tools:text="Uploader" /> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_stream_playlist_grid_item.xml b/app/src/main/res/layout/list_stream_playlist_grid_item.xml index 4b31a452e..1b2ad5dd2 100644 --- a/app/src/main/res/layout/list_stream_playlist_grid_item.xml +++ b/app/src/main/res/layout/list_stream_playlist_grid_item.xml @@ -81,4 +81,15 @@ android:textSize="@dimen/video_item_search_uploader_text_size" tools:text="Uploader" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_stream_playlist_item.xml b/app/src/main/res/layout/list_stream_playlist_item.xml index 193b3fea4..2747038f6 100644 --- a/app/src/main/res/layout/list_stream_playlist_item.xml +++ b/app/src/main/res/layout/list_stream_playlist_item.xml @@ -83,4 +83,15 @@ android:textSize="@dimen/video_item_search_uploader_text_size" tools:text="Uploader" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml index ee047f0da..eea6c5cf0 100644 --- a/app/src/main/res/values-land/dimens.xml +++ b/app/src/main/res/values-land/dimens.xml @@ -14,7 +14,7 @@ 142dp 80dp - 104dp + 106dp 10dp 1sp diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b5e3769ad..cfefd1c2e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -167,7 +167,12 @@ Что нового История поиска Хранить запросы поиска локально - История и кэш + История просмотров + Продолжать воспроизведение + Восстанавливать с последней позиции + Позиции в списках + Отображать индикаторы позиций просмотра в списках + Очистить данные Запоминать воспроизведённые потоки Возобновить при фокусе Возобновлять воспроизведение после перерывов (например, телефонных звонков) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 1577371ef..484543ed2 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -135,8 +135,13 @@ Показувати пропозиції під час пошуку Історія пошуків Зберігати пошукові запити локально - Історія та кеш - Запам\'ятовувати переглянуті відео + Продовживати перегляд + Відновлювати останню позицію + Позиції у списках + Відображати індикатори позицій переглядів у списках + Очистити дані + Вести облік перегляду відеозаписів + Історія переглядів Відновити програвання при поверненні з тла Продовжувати програвання після переривань (наприклад, телефонних дзвінків) Показати пораду \"Утримуй, щоб додати\" diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 37bc9eec6..30bf8f43f 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -44,6 +44,7 @@ + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 94101cfb0..582c4bade 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -15,7 +15,7 @@ 164dp 92dp - 94dp + 96dp 12dp 6dp diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index ec288bb18..49f38b667 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -150,6 +150,8 @@ enable_search_history enable_watch_history main_page_content + enable_playback_resume + enable_playback_state_lists import_data export_data diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb154313e..30a055aa9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -89,7 +89,12 @@ Show suggestions when searching Search history Store search queries locally - History & Cache + Watch history + Resume playback + Restore last playback position + Positions in lists + Show playback position indicators in lists + Clear data Keep track of watched videos Resume on focus gain Continue playing after interruptions (e.g. phone calls) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 51043718a..360a00f93 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -66,6 +66,7 @@ @drawable/toolbar_shadow_light @drawable/light_selector @color/light_ripple_color + @drawable/progress_youtube_horizontal_light @style/PreferenceThemeOverlay.v14.Material @@ -130,6 +131,7 @@ @drawable/toolbar_shadow_dark @drawable/dark_selector @color/dark_ripple_color + @drawable/progress_youtube_horizontal_dark @style/PreferenceThemeOverlay.v14.Material diff --git a/app/src/main/res/values/styles_services.xml b/app/src/main/res/values/styles_services.xml index 257b1905d..d6ab239e4 100644 --- a/app/src/main/res/values/styles_services.xml +++ b/app/src/main/res/values/styles_services.xml @@ -15,18 +15,21 @@ @color/light_soundcloud_primary_color @color/light_soundcloud_dark_color @color/light_soundcloud_accent_color + @drawable/progress_soundcloud_horizontal_light diff --git a/app/src/main/res/xml/history_settings.xml b/app/src/main/res/xml/history_settings.xml index a7428d340..cae2d56c0 100644 --- a/app/src/main/res/xml/history_settings.xml +++ b/app/src/main/res/xml/history_settings.xml @@ -1,40 +1,62 @@ - + android:title="@string/enable_watch_history_title" + app:iconSpaceReserved="false" /> + + + + + android:title="@string/enable_search_history_title" + app:iconSpaceReserved="false" /> - + - + - + - + + + + + \ No newline at end of file