From 398cbe9284a10a2cf7fa9c7524edc89c26014a48 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Tue, 10 Mar 2020 12:06:38 +0300 Subject: [PATCH] Better backstack, better tablet support, switching players confirmation, fix for background playback --- .../fragments/detail/VideoDetailFragment.java | 118 ++++++++++++++---- .../newpipe/player/BackgroundPlayer.java | 2 +- .../org/schabi/newpipe/player/BasePlayer.java | 4 + .../newpipe/player/PopupVideoPlayer.java | 2 +- .../newpipe/player/ServicePlayerActivity.java | 2 +- .../newpipe/player/VideoPlayerImpl.java | 26 ++-- .../event/CustomBottomSheetBehavior.java | 19 +-- .../player/event/PlayerEventListener.java | 2 +- .../newpipe/player/helper/PlayerHelper.java | 4 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/video_audio_settings.xml | 8 ++ 12 files changed, 140 insertions(+), 51 deletions(-) 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 efb997d50..0b7ffddef 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 @@ -18,6 +18,7 @@ import android.widget.*; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.coordinatorlayout.widget.CoordinatorLayout; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; @@ -74,6 +75,7 @@ import org.schabi.newpipe.views.AnimatedProgressBar; import java.io.Serializable; import java.util.Collection; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -86,6 +88,7 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; +import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -237,7 +240,7 @@ public class VideoDetailFragment if (!player.videoPlayerSelected() && !playAfterConnect) return; - if (playerIsNotStopped()) addVideoPlayerView(); + if (playerIsNotStopped() && player.videoPlayerSelected()) addVideoPlayerView(); // If the video is playing but orientation changed let's make the video in fullscreen again if (isLandscape()) checkLandscape(); @@ -382,7 +385,7 @@ public class VideoDetailFragment // Check if it was loading when the fragment was stopped/paused if (wasLoading.getAndSet(false) && !wasCleared()) - selectAndLoadVideo(serviceId, url, name, playQueue); + startLoading(false); } @Override @@ -870,16 +873,10 @@ public class VideoDetailFragment return true; } - StackItem currentPeek = stack.peek(); - if (currentPeek != null && !currentPeek.getPlayQueue().equals(playQueue)) { - // When user selected a stream but didn't start playback this stream will not be added to backStack. - // Then he press Back and the last saved item from history will show up - setupFromHistoryItem(currentPeek); - return true; - } - // If we have something in history of played items we replay it here - boolean isPreviousCanBePlayed = player != null && player.getPlayQueue() != null && player.videoPlayerSelected() + boolean isPreviousCanBePlayed = player != null + && player.getPlayQueue() != null + && player.videoPlayerSelected() && player.getPlayQueue().previous(); if (isPreviousCanBePlayed) { return true; @@ -903,11 +900,15 @@ public class VideoDetailFragment setAutoplay(false); hideMainPlayer(); - selectAndLoadVideo( + setInitialData( item.getServiceId(), item.getUrl(), !TextUtils.isEmpty(item.getTitle()) ? item.getTitle() : "", item.getPlayQueue()); + startLoading(false); + + // Maybe an item was deleted in background activity + if (item.getPlayQueue().getItem() == null) return; PlayQueueItem playQueueItem = item.getPlayQueue().getItem(); // Update title, url, uploader from the last item in the stack (it's current now) @@ -936,7 +937,7 @@ public class VideoDetailFragment return; } setInitialData(serviceId, videoUrl, name, playQueue); - startLoading(false); + startLoading(false, true); } public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) { @@ -965,6 +966,20 @@ public class VideoDetailFragment currentInfo = null; if (currentWorker != null) currentWorker.dispose(); + runWorker(forceLoad, stack.isEmpty()); + } + + private void startLoading(boolean forceLoad, boolean addToBackStack) { + super.startLoading(false); + + initTabs(); + currentInfo = null; + if (currentWorker != null) currentWorker.dispose(); + + runWorker(forceLoad, addToBackStack); + } + + private void runWorker(boolean forceLoad, boolean addToBackStack) { currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -973,12 +988,15 @@ public class VideoDetailFragment hideMainPlayer(); handleResult(result); showContent(); + if (addToBackStack) { + if (playQueue == null) playQueue = new SinglePlayQueue(result); + stack.push(new StackItem(serviceId, url, name, playQueue)); + } if (isAutoplayEnabled()) openVideoPlayer(); }, (@NonNull Throwable throwable) -> { isLoading.set(false); onError(throwable); }); - } private void initTabs() { @@ -1063,7 +1081,9 @@ public class VideoDetailFragment if (append) { NavigationHelper.enqueueOnPopupPlayer(activity, queue, false); } else { - NavigationHelper.playOnPopupPlayer(activity, queue, true); + Runnable onAllow = () -> NavigationHelper.playOnPopupPlayer(activity, queue, true); + if (shouldAskBeforeClearingQueue()) showClearingQueueConfirmation(onAllow); + else onAllow.run(); } } @@ -1073,7 +1093,9 @@ public class VideoDetailFragment VideoStream selectedVideoStream = getSelectedVideoStream(); startOnExternalPlayer(activity, currentInfo, selectedVideoStream); } else { - openNormalPlayer(); + Runnable onAllow = this::openNormalPlayer; + if (shouldAskBeforeClearingQueue()) showClearingQueueConfirmation(onAllow); + else onAllow.run(); } } @@ -1085,7 +1107,9 @@ public class VideoDetailFragment if (append) { NavigationHelper.enqueueOnBackgroundPlayer(activity, queue, false); } else { - NavigationHelper.playOnBackgroundPlayer(activity, queue, true); + Runnable onAllow = () -> NavigationHelper.playOnBackgroundPlayer(activity, queue, true); + if (shouldAskBeforeClearingQueue()) showClearingQueueConfirmation(onAllow); + else onAllow.run(); } } @@ -1579,7 +1603,7 @@ public class VideoDetailFragment && prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true); final boolean showPlaybackPosition = prefs.getBoolean( activity.getString(R.string.enable_playback_state_lists_key), true); - if (!playbackResumeEnabled || info.getDuration() <= 0) { + if (!playbackResumeEnabled) { if (playQueue == null || playQueue.getStreams().isEmpty() || playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET || !showPlaybackPosition) { positionView.setVisibility(View.INVISIBLE); @@ -1605,7 +1629,8 @@ public class VideoDetailFragment }, e -> { if (DEBUG) e.printStackTrace(); }, () -> { - // OnComplete, do nothing + animateView(positionView, false, 0); + animateView(detailPositionView, false, 0); }); } @@ -1615,6 +1640,10 @@ public class VideoDetailFragment positionView.setMax(durationSeconds); positionView.setProgress(progressSeconds); detailPositionView.setText(Localization.getDurationString(progressSeconds)); + if (positionView.getVisibility() != View.VISIBLE) { + animateView(positionView, true, 100); + animateView(detailPositionView, true, 100); + } } /*////////////////////////////////////////////////////////////////////////// @@ -1628,11 +1657,11 @@ public class VideoDetailFragment // information about deleted/added items inside Channel/Playlist queue and makes possible to have a history of played items if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(queue)) { stack.push(new StackItem(serviceId, url, name, playQueue)); - } else if (stack.peek().getPlayQueue().equals(queue)) { + } else if (findQueueInStack(queue) != null) { // On every MainPlayer service's destroy() playQueue gets disposed and no longer able to track progress. // That's why we update our cached disposed queue with the new one that is active and have the same history // Without that the cached playQueue will have an old recovery position - stack.peek().setPlayQueue(queue); + findQueueInStack(queue).setPlayQueue(queue); } if (DEBUG) { @@ -1671,20 +1700,23 @@ public class VideoDetailFragment } @Override - public void onMetadataUpdate(StreamInfo info) { - if (!stack.isEmpty()) { + public void onMetadataUpdate(StreamInfo info, PlayQueue queue) { + StackItem item = findQueueInStack(queue); + if (item != null) { // When PlayQueue can have multiple streams (PlaylistPlayQueue or ChannelPlayQueue) every new played stream gives // new title and url. StackItem contains information about first played stream. Let's update it here - StackItem peek = stack.peek(); - peek.setTitle(info.getName()); - peek.setUrl(info.getUrl()); + item.setTitle(info.getName()); + item.setUrl(info.getUrl()); } + // They are not equal when user watches something in popup while browsing in fragment and then changes screen orientation + // In that case the fragment will set itself as a service listener and will receive initial call to onMetadataUpdate() + if (!queue.equals(playQueue)) return; updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) return; currentInfo = info; - setInitialData(info.getServiceId(), info.getUrl(),info.getName(), playQueue); + setInitialData(info.getServiceId(), info.getUrl(),info.getName(), queue); setAutoplay(false); prepareAndHandleInfo(info, true); } @@ -1852,6 +1884,37 @@ public class VideoDetailFragment return url == null; } + private StackItem findQueueInStack(PlayQueue queue) { + StackItem item = null; + Iterator iterator = stack.descendingIterator(); + while (iterator.hasNext()) { + StackItem next = iterator.next(); + if (next.getPlayQueue().equals(queue)) { + item = next; + break; + } + } + return item; + } + + private boolean shouldAskBeforeClearingQueue() { + PlayQueue activeQueue = player != null ? player.getPlayQueue() : null; + // Player will have STATE_IDLE when a user pressed back button + return isClearingQueueConfirmationRequired(activity) && playerIsNotStopped() + && activeQueue != null && !activeQueue.equals(playQueue) && activeQueue.getStreams().size() > 1; + } + + private void showClearingQueueConfirmation(Runnable onAllow) { + new AlertDialog.Builder(activity) + .setTitle(R.string.confirm_prompt) + .setMessage(R.string.clear_queue_confirmation_description) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + onAllow.run(); + dialog.dismiss(); + }).show(); + } + /* * Remove unneeded information while waiting for a next task * */ @@ -1905,6 +1968,7 @@ public class VideoDetailFragment && player != null && player.isPlaying() && !player.isInFullscreen() + && !PlayerHelper.isTablet(activity) && player.videoPlayerSelected(); if (needToExpand) player.toggleFullscreen(); break; diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index ab07ded22..e408f49f6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -451,7 +451,7 @@ public final class BackgroundPlayer extends Service { private void updateMetadata() { if (activityListener != null && getCurrentMetadata() != null) { - activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 815cbb8ab..490419c64 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -1236,6 +1236,10 @@ public abstract class BasePlayer implements return simpleExoPlayer != null && simpleExoPlayer.isPlaying(); } + public boolean isLoading() { + return simpleExoPlayer != null && simpleExoPlayer.isLoading(); + } + @Player.RepeatMode public int getRepeatMode() { return simpleExoPlayer == null 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 0a15e7169..22681d9ce 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -673,7 +673,7 @@ public final class PopupVideoPlayer extends Service { private void updateMetadata() { if (activityListener != null && getCurrentMetadata() != null) { - activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); } } 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 1c449c77e..619200727 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -596,7 +596,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } @Override - public void onMetadataUpdate(StreamInfo info) { + public void onMetadataUpdate(StreamInfo info, PlayQueue queue) { if (info != null) { metadataTitle.setText(info.getName()); metadataArtist.setText(info.getUploaderName()); diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java index d026ff8cf..efbe06457 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -761,7 +761,8 @@ public class VideoPlayerImpl extends VideoPlayer private void setupScreenRotationButton() { boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service); - boolean showButton = (orientationLocked || isVerticalVideo || isTablet(service)) && videoPlayerSelected(); + boolean tabletInLandscape = isTablet(service) && service.isLandscape(); + boolean showButton = videoPlayerSelected() && (orientationLocked || isVerticalVideo || tabletInLandscape); screenRotationButton.setVisibility(showButton ? View.VISIBLE : View.GONE); screenRotationButton.setImageDrawable(service.getResources().getDrawable( isInFullscreen() ? R.drawable.ic_fullscreen_exit_white : R.drawable.ic_fullscreen_white)); @@ -1048,10 +1049,10 @@ public class VideoPlayerImpl extends VideoPlayer useVideoSource(true); break; case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED: - // This will be called when user goes to another app - // We don't want to interrupt playback and don't want to see notification if player is stopped - // since next lines of code will enable background playback if needed - if (videoPlayerSelected() && isPlaying()) { + // This will be called when user goes to another app/activity, turns off a screen. + // We don't want to interrupt playback and don't want to see notification if player is stopped. + // Next lines of code will enable background playback if needed + if (videoPlayerSelected() && (isPlaying() || isLoading())) { if (backgroundPlaybackEnabled()) { useVideoSource(false); } else if (minimizeOnPopupEnabled()) { @@ -1076,14 +1077,15 @@ public class VideoPlayerImpl extends VideoPlayer break; case Intent.ACTION_SCREEN_ON: shouldUpdateOnProgress = true; - // Interrupt playback only when screen turns on and user is watching video in fragment - if (backgroundPlaybackEnabled() && getPlayer() != null && (isPlaying() || getPlayer().isLoading())) + // Interrupt playback only when screen turns on and user is watching video in popup player + // Same action for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED event + if (backgroundPlaybackEnabled() && popupPlayerSelected() && (isPlaying() || isLoading())) useVideoSource(true); break; case Intent.ACTION_SCREEN_OFF: shouldUpdateOnProgress = false; - // Interrupt playback only when screen turns off with video working - if (backgroundPlaybackEnabled() && getPlayer() != null && (isPlaying() || getPlayer().isLoading())) + // Interrupt playback only when screen turns off with popup player working + if (backgroundPlaybackEnabled() && popupPlayerSelected() && (isPlaying() || isLoading())) useVideoSource(false); break; } @@ -1330,7 +1332,7 @@ public class VideoPlayerImpl extends VideoPlayer AppCompatActivity parent = getParentActivity(); boolean videoInLandscapeButNotInFullscreen = service.isLandscape() && !isInFullscreen() && videoPlayerSelected() && !audioOnly; boolean playingState = getCurrentState() != STATE_COMPLETED && getCurrentState() != STATE_PAUSED; - if (parent != null && videoInLandscapeButNotInFullscreen && playingState) + if (parent != null && videoInLandscapeButNotInFullscreen && playingState && !PlayerHelper.isTablet(service)) toggleFullscreen(); setControlsSize(); @@ -1695,10 +1697,10 @@ public class VideoPlayerImpl extends VideoPlayer private void updateMetadata() { if (fragmentListener != null && getCurrentMetadata() != null) { - fragmentListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); + fragmentListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); } if (activityListener != null && getCurrentMetadata() != null) { - activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java index c7acc0390..9d4707666 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java @@ -35,14 +35,17 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior // Found that user still swiping, continue following if (skippingInterception) return false; - // Without overriding scrolling will not work when user touches these elements - for (Integer element : skipInterceptionOfElements) { - ViewGroup viewGroup = child.findViewById(element); - if (viewGroup != null) { - visible = viewGroup.getGlobalVisibleRect(globalRect); - if (visible && globalRect.contains((int) event.getRawX(), (int) event.getRawY())) { - skippingInterception = true; - return false; + // Don't need to do anything if bottomSheet isn't expanded + if (getState() == BottomSheetBehavior.STATE_EXPANDED) { + // Without overriding scrolling will not work when user touches these elements + for (Integer element : skipInterceptionOfElements) { + ViewGroup viewGroup = child.findViewById(element); + if (viewGroup != null) { + visible = viewGroup.getGlobalVisibleRect(globalRect); + if (visible && globalRect.contains((int) event.getRawX(), (int) event.getRawY())) { + skippingInterception = true; + return false; + } } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java index 37ad9798f..8741f539f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java @@ -10,6 +10,6 @@ public interface PlayerEventListener { void onQueueUpdate(PlayQueue queue); void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters); void onProgressUpdate(int currentProgress, int duration, int bufferPercent); - void onMetadataUpdate(StreamInfo info); + void onMetadataUpdate(StreamInfo info, PlayQueue queue); void onServiceStopped(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index ae1cac382..6afb5a322 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -200,6 +200,10 @@ public class PlayerHelper { return isAutoQueueEnabled(context, false); } + public static boolean isClearingQueueConfirmationRequired(@NonNull final Context context) { + return getPreferences(context).getBoolean(context.getString(R.string.clear_queue_confirmation_key), false); + } + @MinimizeMode public static int getMinimizeOnExitAction(@NonNull final Context context) { final String defaultAction = context.getString(R.string.minimize_on_exit_none_key); diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 404397450..f2e957a71 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -27,6 +27,7 @@ auto_queue_key screen_brightness_key screen_brightness_timestamp_key + clear_queue_confirmation_key seek_duration 10000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3a575ae25..388897ffd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -71,6 +71,9 @@ Use fast inexact seek Inexact seek allows the player to seek to positions faster with reduced precision Fast-forward/-rewind seek duration + Ask confirmation before clearing a queue + After switching from one player to another your queue may be replaced + Queue from the active player will be replaced Load thumbnails Show comments Disable to stop showing comments diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index 447fa9018..88dc071d0 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -164,5 +164,13 @@ android:key="@string/seek_duration_key" android:summary="%s" android:title="@string/seek_duration_title"/> + + +